Unix/Linux Stuff: pipe, shared memory & more fork
fork() allows a process/program to duplicate itself to create a brand new process. This process is basically a child of the calling process (parent). The child has its own process ID which gets returns to the parent upon creation while it returns 0 on its own process. Let's say we created:
int main(){
printf("I am parent. My PID is: %d\n", getpid());
int pid = fork();
if(pid == 0){
printf("I am child. My PID is %d\n", getpid());
exit(0);
} else {
printf("My child's PID is: %d\n", pid);
}
return 0;
}
The code above may print something like this:
I am parent. My PID is: 29321 My child's PID is: 29322 I am child. My PID is 29322
But there is something to this that we must keep in mind. The termination process must be in sequence: child must die first, then parent - its calling process. If parent dies first, then child becomes a zombie. A zombie is a process that doesn't have a parent. When a child quits, it reports its termination to its parent. If no parent waits to collect the exit status of the child, it gets confused and becomes a ghost process that can be seen as a process but not killed. The child process remains in memory as a zombie. Eventually, as more zombies spawn, it takes up more resources which eventually leads to crashes, etc. In order to avoid this from happening, we must make the parent wait. Thankfully, there is a simple command called wait(). We use it like this:
int main(){
printf("I am parent. My PID is: %d\n", getpid());
int pid = fork();
int wait;
if(pid == 0){
printf("I am child. My PID is %d\n", getpid());
exit(0);
} else {
printf("My child's PID is: %d\n", pid);
}
wait(&status);
return 0;
}
wait() stores the status information to status, it returns the process ID of the terminated child, and -1 on error as usual. Now, parent waits for child to end, the whole program ends in sequence.
Shared memory is a way for parent and child to share data or use the same portion of memory. It's very easy to use:
main()
{
int* i, status, id;
id=shmget((key_t)IPC_PRIVATE,4,0666|IPC_CREAT);
i=(int *)shmat(id,0,0);
*i=4;
printf("Parent *i is %d\n",*i);
if(!fork()){
int* j;
j=(int *)shmat(id,0,0);
printf("Child *j is %d\n",*j);
(*j)++;
printf("Child *j is %d\n",*j);
shmdt(j);
exit(0);
}
//wait(&status);
(*i)++;
printf("Parent *i is %d\n",*i);
shmdt(i);
}
There are three functions you must use for shared memory:
- shmget() - allocates a shared memory segment. The first argument accepts a key_t value. IPC_PRIVATE is a special key value used to create a new memory segment. The IPC_PRIVATE name is a bit confusing considering that it is to be used to create new memory segment. This is apparently a bug for poor choice of name because IPC_NEW would clearly make more sense. The next argument is the number of bytes we want the shared memory to have. Since we are going to share an integer, we pass 4 to it - since an int is worth 4 bytes. Finally the last argument is the permission for new segment. On success, shmget() returns the ID to the shared memory, -1 on fail.
- shmat() - this function is used to attach ourselves to the shared memory. We use the int pointer i we created to point to the address of shared memory. The first argument of shmat() is the ID of shared memory, the last two are used for more complex ones that accept shared memory address and flags. We pass 0 to those since we're not gonna use them. shmat() returns -1 on fail, or pointer to the shared memory on success.
- shmdt() - finally, this function is used to detach ourselves to the shared memory. It only accepts an ID to the shared memory. Returns 0 on success, -1 otherwise.
Notice that I commented out wait(). Our expected output should be something like this:
Parent *i is 4 Child *j is 4 Child *j is 5 Parent *i is 6
But since we don't wait for the child the die, the output may be different than we expect. The parent may print the second printf() first before the child could and such like that. On the top of that, zombie processes will get created. That is why it is essential that we must keep the termination order of parent and child in proper sequence, or unexpected results may occur.
Another way for parent and child to share or pass data to one another is with the use of pipe(). Think of a pipe in a literal way - a tunnel that can be used for various things, such as a tunnel between a parent and child process!
int main() {
int bytes_processed;
int pipefd[2];
char datapassed[] = "123";
char buffer[BUFSIZ + 1] = "";
int fork_result;
if (pipe(pipefd) == 0) {
if ((fork_result = fork()) == 0) {
bytes_processed = read(pipefd[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", bytes_processed, buffer);
exit(EXIT_SUCCESS);
}
else if (fork_result > 0) {
bytes_processed = write(pipefd[1], datapassed, strlen(datapassed));
printf("Wrote %d bytes\n", bytes_processed);
exit(EXIT_SUCCESS);
}
}
exit(EXIT_FAILURE);
}
The first thing we must have is an array that will hold two file descriptors: pipefd[2] - one for reading, and the other for writing. To create a pipe, simply call the pipe() function and pass that pipefd array. pipefd[0] contains file descriptor for reading, and pipefd[1] for writing. As you can see inside the child, we use the function we are familiar with: read() - read() is more or less the same as recv(), it accepts a file descriptor, buffer for reading, and the max size to read. Since the child is an exact copy of the parent, it should have the pipe that we created in the parent. We use the reading side of the pipe, pipefd[0]. The child will basically read whatever the parent writes to the writing side of the pipe. On the parent side, we have write(), which is again more or less the same as send().
Note that pipes can be used in many various things. Just as you use pipes in commands (eg. ls | wc), this is pretty much the coding side of it.
That's all, I hope you like it~