fork系统调用 fork系统调用:创建新进程
1 2 3 4 5 6 7 8 9 #include <sys/types.h> #include <unistd.h> pid_t fork (void ) ;
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性与原进程相同,比如堆栈指针、标志寄存器的值。不同的属性有:该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
子进程的代码与父进程完全相同,同时会复制父进程的数据(堆数据、栈数据、静态数据)。数据的复制采用写时复制,读时共享 。只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)
子进程fork创建后,父进程打开的文件描述符默认在子进程中也是打开的,而且文件描述符的引用计数加一。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
exec系列系统调用 exec系列函数:在子进程中执行其他程序,即替换当前进程映像。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <unistd.h> extern char **environ;int execl (const char *path,const char *arg,...) ; int execlp (const char *file,const char *arg,...) ;int execle (const char *path,const char *arg,...,char *const envp[]) ;int execv (const char *path,char *const argv[]) ; int execvp (const char *file,char *const argv[]) ; int execve (const char *path,char *const argv[],char *const envp[]) ;
exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性。
处理僵尸进程 子进程处于僵尸态:
在子进程结束运行之后,父进程读取其退出状态之前,该子进程处于僵尸态。
父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。 init进程接管了该子进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
父进程未正确地处理子进程的返回信息,子进程都会停留在僵尸态,并占据着内核资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <sys/types.h> #include <sys/wait.h> pid_t wait (int * stat_loc) ; pid_t waitpid (pid_t pid, int * stat_loc, int options) ;
在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。waitpid函数最好在某个子进程退出之后再调用它。
利用SIGCHLD信号,父进程可以得知某个子进程已经退出。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号。可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程:
1 2 3 4 5 6 7 static void handle_child (int sig) { pid_t pid; int stat; while ((pid = waitpid (-1 , &stat, WNOHANG)) > 0 ) { } }
管道 父进程与子进程间通信的常用手段:管道。
管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭 fd[0],另一个关闭fd[1]。
如果要实现父、子进程之间的双向数据传输,就必须使用两个管道。
父进程通过管道向子进程写数据:
socket编程接口提供了一个创建全双工管道的系统调用:socketpair。
管道只能用于有关联的两个进程(比如父、子进程)间的通信。System V IPC能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条信道。
FIFO管道(First In First Out,先进先出),特殊的管道,也能用于无关联进程之间的通信。
信号量 临界区/关键代码段:程序对共享资源的访问的代码,这段代码引发了进程之间的竞态条件。
信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。在Linux/UNIX中,“等待”和“信号”都已经具有特殊的含义,所以对信号量的这两种操作更常用的称呼是P、V操作。P:传递,进入临界区;V:释放,退出临界区。假设有信号量SV,则对它的P、V操作含义如下:
P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。
最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。
当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码段。
Linux信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop和semctl。
semget系统调用 semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/sem.h> int semget (key_t key,int num_sems,int sem_flags) ;
semop系统调用 semop系统调用改变信号量的值,即执行P、V操作。
1 2 3 4 5 unsigned short semval; unsigned short semzcnt; unsigned short semncnt; pid_t sempid;
semop对信号量的操作实际上就是对内核变量的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <sys/sem.h> int semop (int sem_id,struct sembuf*sem_ops,size_t num_sem_ops) ;struct sembuf { unsigned short int sem_num; short int sem_op; short int sem_flg; }
semctl系统调用 semctl系统调用允许调用者对信号量进行直接控制。
1 2 3 4 5 6 7 8 9 10 11 #include <sys/sem.h> int semctl (int sem_id,int sem_num,int command,...) ;
共享内存 共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。
缺点:必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。
Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmget、shmat、shmdt和shmctl。
shmget系统调用 shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <sys/shm.h> int shmget (key_t key,size_t size,int shmflg) ;
shmat和shmdt系统调用 共享内存被创建/获取之后,还不能立即访问它,而是需要先将它关联到进程的地址空间中。
使用完共享内存之后,需要将它从进程地址空间中分离。这两项任务分别由如下两个系统调用实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <sys/shm.h> void * shmat (int shm_id,const void *shm_addr,int shmflg) ; int shmdt (const void *shm_addr) ;
shmctl系统调用 shmctl系统调用控制共享内存的某些属性。
1 2 3 4 5 6 7 8 9 10 #include <sys/shm.h> int shmctl (int shm_id,int command,struct shmid_ds*buf) ;
共享内存的POSIX方法 mmap函数,利用MAP_ANONYMOUS标志可以实现父、子进程之间的匿名内存共享。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象:
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> int shm_open (const char *name,int oflag,mode_t mode) ;
和打开的文件最后需要关闭一样,由shm_open创建的共享内存对象使用完之后也需要被删除。这个过程是通过如下函数实现的:
1 2 3 4 5 6 7 8 9 #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> int shm_unlink (const char *name) ;
如果代码中使用了上述POSIX共享内存函数,则编译的时候需要指定链接选项-lrt。
消息队列 消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据。
Linux消息队列的API都定义在sys/msg.h头文件中,包括4个系统调用:msgget、msgsnd、msgrcv和msgctl。
msgget系统调用 msgget系统调用创建一个消息队列,或者获取一个已有的消息队列。
1 2 3 4 5 6 7 8 9 10 #include <sys/msg.h> int msgget (key_t key,int msgflg) ;
如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建并初始化。
msgsnd系统调用 msgsnd系统调用把一条消息添加到消息队列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <sys/msg.h> int msgsnd (int msqid,const void *msg_ptr,size_t msg_sz,int msgflg) ;struct msgbuf { long mtype; char mtext[512 ]; };
msgrcv系统调用 msgrcv系统调用从消息队列中获取消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <sys/msg·h> int msgrcv (int msqid,void *msg_ptr,size_t msg_sz,long int msgtype,int msgflg) ;
msgctl系统调用 msgctl系统调用控制消息队列的某些属性。
1 2 3 4 5 6 7 8 9 10 11 #include <sys/msg.h> int msgctl (int msqid,int command,struct msqid_ds*buf) ;
IPC命令 信号量、共享内存、消息队列,这三种System V IPC进程间通信方式都使用一个全局唯一的键值(key)来描述一个共享资源。当程序调用semget、shmget或者msgget时,就创建了这些共享资源的一个实例。
Linux提供了ipcs命令,以观察当前系统上拥有哪些共享资源实例。
可以使用ipcrm命令来删除遗留在系统中的共享资源。
在进程间传递文件描述符 由于fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。
如何把子进程中打开的文件描述符传递给父进程,或者说,如何在两个不相干的进程之间传递文件描述符,可以利用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递。
在进程间传递文件描述符:子进程中打开一个文件描述符,然后将它传递给父进程,父进程则通过读取该文件描述符来获得文件的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <sys/socket.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <string.h> static const int CONTROL_LEN = CMSG_LEN ( sizeof (int ) );void send_fd ( int fd, int fd_to_send ) { struct iovec iov [1]; struct msghdr msg ; char buf[0 ]; iov[0 ].iov_base = buf; iov[0 ].iov_len = 1 ; msg.msg_name = NULL ; msg.msg_namelen = 0 ; msg.msg_iov = iov; msg.msg_iovlen = 1 ; cmsghdr cm; cm.cmsg_len = CONTROL_LEN; cm.cmsg_level = SOL_SOCKET; cm.cmsg_type = SCM_RIGHTS; *(int *)CMSG_DATA ( &cm ) = fd_to_send; msg.msg_control = &cm; msg.msg_controllen = CONTROL_LEN; sendmsg ( fd, &msg, 0 ); } int recv_fd ( int fd ) { struct iovec iov [1]; struct msghdr msg ; char buf[0 ]; iov[0 ].iov_base = buf; iov[0 ].iov_len = 1 ; msg.msg_name = NULL ; msg.msg_namelen = 0 ; msg.msg_iov = iov; msg.msg_iovlen = 1 ; cmsghdr cm; msg.msg_control = &cm; msg.msg_controllen = CONTROL_LEN; recvmsg ( fd, &msg, 0 ); int fd_to_read = *(int *)CMSG_DATA ( &cm ); return fd_to_read; } int main () { int pipefd[2 ]; int fd_to_pass = 0 ; int ret = socketpair ( PF_UNIX, SOCK_DGRAM, 0 , pipefd ); assert ( ret != -1 ); pid_t pid = fork(); assert ( pid >= 0 ); if ( pid == 0 ) { close ( pipefd[0 ] ); fd_to_pass = open ( "test.txt" , O_RDWR, 0666 ); send_fd ( pipefd[1 ], ( fd_to_pass > 0 ) ? fd_to_pass : 0 ); close ( fd_to_pass ); exit ( 0 ); } close ( pipefd[1 ] ); fd_to_pass = recv_fd ( pipefd[0 ] ); char buf[1024 ]; memset ( buf, '\0' , 1024 ); read ( fd_to_pass, buf, 1024 ); printf ( "I got fd %d and data %s\n" , fd_to_pass, buf ); close ( fd_to_pass ); }