【读书笔记】Linux高性能服务器编程(第二篇 第十三章)

第13章 多进程编程

进程是Linux操作系统环境的基础,控制着系统上几乎所有的活动。

 

13.1 fork 系统调用

#include<sys/types.h>

#include<unistd.h>

pid_t  fork ( void ) ;

该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0

该返回值是后续代码判断当前进程是父进程还是子进程的依据。

fork 调用失败时返回 -1 , 并设置 errno 

fork 函数复制当前进程,在内核进程表中创建一个新的进程表项,

新的进程表项有很多属性和原进程相同(例如:栈指针,堆指针和标志寄存器的值),但也有许多属性被赋予新的值(例如:该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用))

 

子进程的代码与父进程完全相同,同时子进程还会复制父进程的数据(堆数据,栈数据和静态数据)

数据的复制采用的是写时复制,即只有任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。

注意:如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没有必要的内存分配和数据复制。

创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加,而且父进程的用户根目录,当前工作目录等变量的引用计数也会加

 

13.2 exec系列系统调用

#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[ ]);

path参数指定可执行文件的完整路径。

file参数可以接受文件名,该文件的具体位置则在环境变量PATH中搜寻。

arg参数接受可变参数,argv则坚守参数数组,它们都会被传递给新程序的main参数。

envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。

一般情况下,exec函数是不返回的,除非出错。出错时返回 -1,并设置errno 。 如果没出错,则原程序中exec调用之后的代码都不会执行,因为此时原程序已经被exec的参数指定的程序完全替换(包括代码和数据)。

注意:exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性。

 

13.3 处理僵尸进程

对于多进程程序而言,父进程一般需要跟踪子进程的退出状态,因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。

子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态

另一种使子进程进入僵尸态的情况是:父进程结束或者异常终止,而子进程继续运行,此时子进程的PPID将被设置为1init进程)。init进程接管了该子进程,并等待它结束。

父进程退出之后,子进程退出之前,该子进程处于僵尸态

由此可见,无论哪种情况,如果父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源(这绝不能容许,因为内核资源有限)。

下面的函数在父进程调用,以获取子进程的返回信息,避免僵尸进程的产生,或者使子进程的僵尸态立即结束:

#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 ) ;

wait函数将阻塞进程,直到该进程的某个子进程结束运行为止,它返回结束运行的子进程啊的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。

sys/wait.h 头文件中定义了几个宏来帮助解释子进程的退出状态信息,如表:


wait函数的阻塞特性显然不是服务器程序期望的,而waitpid函数解决了这个问题。

waitpid只等待由pid参数指定的子进程,如果pid取值为 -1,那么它就和wait函数相同,(等待任意一个子进程结束)。

stat_loc参数的含义和wait函数的stat_loc参数相同。

options参数可以控制waitpid函数的行为,该参数最常用的值是WNOHANG。当option的取值是WNOHANG时,waitpid调用将是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正常退出了,则waitpid返回该子进程的PID。 

waitpid调用失败时返回-1,并设置errno 

注意:

在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。

waitpid函数而言,我们最好在某个子进程退出之后再调用它。

那么父进程如何得知某个子进程已经退出了呢?

通过SIGCHLD信号,当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,父进程可以捕获SIGCHLD信号并在信号处理函数中处理该子进程。

 

13.4 管道

管道(pipe)只能在父子进程间传递数据,利用的是fork调用之后两个管道文件描述符( fd[0] 读 和fd[1]写 )。

有一种特殊的管道称为有名管道(FIFO),能用于无关联进程之间的通信。

注意:一对这样的文件描述符只能保证父子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0](读),另一个关闭fd[1](写)

如果要实现父子进程之间的双向数据传输,必须使用两个管道

 

13.5  信号量

13.5.1  信号量原语

当多个进程同时访问系统上的某个资源的时候,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。

通常,程序对共享资源的访问的代码只是很短的一段,我们称这段代码为关键代码段(临界区)。

对进程同步也就是确保任一时刻只有一个进程能进入关键代码段。

信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:在Linux/UNIX对信号量的两种操作称为PV操作。

设有信号量SV

PSV)使SV减 ,若SV0,则挂起进程的执行。

VSV),若有其他进程因为等待信号量而挂起,则唤醒之;如果没有则将SV加 

信号量的取值可以是任何自然数,但是最常用的信号量是二进制信号量,只能取01两个值。

注意:使用一个普通变量来模拟二进制信号量是行不通的,因为所有高级语言都没有一个原子操作可以同时完成如下两步操作:检测变量是否为true/false,如果是则再将它设置为false/true 

13.5.2  semget系统调用

#include <sys/sem.h>

int semget ( key_t key , int num_sems , int sem_flags ) ;

key参数是一个键值,用来标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建 获取该信号量

num_sems参数指定要创建 / 获取的信号量集中信号量的数目,如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为

sem_flags参数指定一组标志,它的低9位是该信号量的权限 ,其格式和含义都与系统调用openmode参数相同 。此外它还可以和IPC_CREAT标志做按位“或”运算以创建新的信号量集。此时即使信号量已经存在,semget也不会产生错误

我们可以联合使用IPC_CREAT | IPC_EXCL标志来来确保创建一组新的,唯一的信号量集。在这种情况下,如果信号量集已经存在,则semget返回错误并设置errnoEEXIST 

semget成功时返回一个正整数值,它是信号量集的标识符;semget失败时返回 -1 ,并设置errno 

如果semget用于创建信号量集,则与之关联的内核的数据结构体semid_ds将被创建并初始化。

semid_ds结构体的定义如下:

#include <sys/sem.h>

struct ipc_perm{

   key_t  key ;  // 键值

   uid_t  uid ;  // 所有者的有效用户ID

   gid_t  gid ;  // 所有者的有效组ID

   uid_t  cuid ;  // 创建者的有效用户ID

   gid_t  cgid ;  // 创建者的有效组ID

   mode_t  mode ;  // 访问权限

} ;

struct  semid_ds {

  struct ipc_perm  sem_perm ;  //信号量的操作权限

  unsigned long int  sem_nsems ; //该信号量集中的信号量数目

  time_t  sem_otime ;  //最后一次调用semop的时间 

  time_t  sem_ctime ;  //最后一次调用semctl的时间

} ; 

semgetsemid_ds结构体的初始化包括:

sem_perm.cuidsem_perm.uid设置为调用进程的有效用户ID

sem_perm.cgid 和 sem_perm.gid设置为调用进程的有效组ID

sem_perm.mode的最低9位设置为sem_flags参数的最低9位。

sem_nsems设置为num_sems

sem_otime设置为0

sem_ctime设置为当前的系统时间。

 

13.5.3 semop系统调用

semop改变信号量的值,即执行PV操作。

与每个信号量关联的一些重要内核变量:

unsigned short semval ;   //信号量的值

unsigned short semzcnt ;  //等待信号量的值变为0的进程数量

unsigned short semncnt ;  //等待信号量值增加的进程数量

pid_t  sempid ;    //最后一次执行semop操作的进程ID

semop定义:

#include <sys/sem.h>

int semop ( int sem_id , struct sembuf* sem_ops , size_t num_sem_ops ) ;

sem_id 参数是由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集 。

sem_ops参数指向一个sembuf结构体类型的数组,定义如下:

struct sembuf {

   unsigned short int sem_num ;

   short int  sem_op ;

   short int  sem_flg ;

};

其中,sem_num成员是信号量集中信号量的编号,0表示信号量集中的第一个信号量

sem_op成员指定操作类型,其可选值为正整数0负整数

每种类型的操作的行为又受到sem_flg成员的影响。

sem_flg的可选值是IPC_NOWAITSEM_UNDO

IPC_NOWAIT:无论信号量操作是否成功,semop调用都将立即返回,类似于非阻塞的I/O操作。

SEM_UNDO:当进程退出时,取消正在进行的semop操作

sem_opsem_flg将按照如下方式来影响semop的行为:

1)如果sem_op大于0,则semop将被操作的信号量的值semval增加sem_op 。该操作要求调用进程对被操作信号量集拥有写权限。此时若设置了SEM_UNDO标志,则系统将更新进程的semadj变量(用以跟踪进程对信号量的修改情况)。

2)如果sem_op等于0,则表示这是一个“等待0”操作,该操作要求调用进程对被操作信号量集拥有读权限。

如果此时信号量的值是0,则调用立即成功返回。

如果此时信号量的值不是0,则semop失败返回或者阻塞进程以等待信号量变为。在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errnoEAGAIN 

如果未指定IPC_NOWAIT标志,则信号量的semzcnt值加1,进程被投入睡眠直到下列3个条件之一发生:

1.信号量的值semval变为0,此时系统将信号量semzcnt值减1

2.被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM 

3.调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量的semzcnt值减

3)如果sem_op小于0,则表示对信号量值进行减操作,即期望获得信号量,该操作要求调用进程对被操作信号量集拥有写权限。

如果信号量的值semval大于或等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval值减去sem_op的绝对值。此时如果设置了SEM_UNDO标志,则系统将更新进程的semadj变量。

如果信号量的值semval小于sem_op的绝对值,则semop失败返回或者阻塞进程以等待信号量可用。在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errnoEAGAIN 。如果未设置IPC_NOWAIT标志,则信号量的semcnt值加1,进程被投入睡眠直到下列3个条件之一发生:

1.信号量的值semval变得大于或等于sem_op的绝对值,此时系统将该信号量的semncnt值减1,并将semval减去sem_op的绝对值,同时如果SEM_UNDO标志被设置,则系统更新semadj变量。

2.被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM

3.调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量semncnt值减

 

semop系统调用的第3个参数num_sem_ops指定要执行的操作个数,即sem_ops数组中元素的个数。semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,并且该过程是原子操作,以避免别的进程在同一时刻按照不同的顺序对该信号集中的信号量执行semop操作导致的竞态条件。

semop成功时返回0,失败返回 -1并设置errno 

失败的时候,sem_ops数组中指定的所有操作都不被执行。

 

13.5.4  semctl 系统调用

semctl系统调用允许调用者对信号量进行直接控制,定义:

#include <sys/sem.h>

int semctl ( int sem_id , int sem_num , int command , ... ) ;

sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集。

sem_num参数指定被操作的信号量在信号量集中的编号。

command参数指定要执行的命令,有的命令需要调用者传递第4个参数。

4个参数的推荐格式:

union semun{

   int val ;    // 用于SETVAL命令

   struct semid_ds*  buf ;  //用于IPC_STATIPC_SET命令

   unsigned short*  array ;  //用于GETALLSETALL命令

   struct seminfo*  __buf ;  //用于IPC_INFO命令

} ;

 

struct  seminfo {

int  semmap ; //Linux内核没有使用

int  semmni ;  //系统最多可以拥有的信号量集数目

int  semmns ;  //系统最多可以拥有的信号量数目

int  semmnu;  //Linux内核没有使用

int  semmsl ;  //一个信号量集最多允许包含的信号量数目

    int  semopm ; //semop一次最多能执行的sem_op操作数目

    int  semume; //Linux内核没有使用

int  semusz ; //sem_undo结构体的大小

int  semvmx; //最大允许的信号量值

    int  semaem ; 最多允许的UNDO次数

};

 

 

semctl支持的所有命令如表:


setctl成功时返回值取决于command参数,semctl失败时返回-1并设置errno 

 

13.5.5 特殊键值IPC_PRIVATE

semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量。

 

 

13.6  共享内存

共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输

注意:我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件,因此,共享内存通常和其他进程间通信方式一起使用

 

13.6.1  shmget系统调用

shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。

#include <sys/shm.h>

int shmget ( key_t key , size_t size , int shmflg ) ;

key参数是一个键值,用来标识一段全局唯一的共享内存。

size参数指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size值必须被指定。如果是获取已经存在的共享内存,则可以把size设置为

shmflg参数的使用和含义与semget系统调用的sem_flags参数相同,不过shmget支持两个额外的标志——SHM_HUGETLBSHM_NORESERVE 

SHM_HUGETLB:类似于mmapMAP_HUGETLB标志,系统将使用“大页面”来为共享内存分配空间。

SHM_NORESERVE:类似于mmapMAP_NORESERVE标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写操作将触发SIGSEGV信号。

shmget成功时返回一个正整数值,它是共享内存的标识符。

shmget失败时返回 -1,并设置errno 

如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化。

 

13.6.2  shmatshmdt系统调用

共享内存被创建 获取之后,我们需要先将它关联到进程的地址空间中,使用完共享内存之后,我们也需要将它从进程地址空间中分离。

#include <sys/shm.h>

void* shmat ( int shm_id , const void* shm_addr , int shmflg );

int  shmdt ( const void* shm_addr ) ;

其中shm_id参数是由shmget调用返回的共享内存标识符。

sgm_addr参数指定将共享内存关联到进程的哪块空间,最终的效果还受到shmflg参数的可选标志SHM_RND的影响:

1)如果shm_addrNULL,则被关联的地址由操作系统选择,以确保代码的可移植性。

2)如果shm_addr非空,并且SHM_RND标志未被设置,则共享内存被关联到addr指定的地址处。

3)如果shm_addr非空,并且设置了SHM_RND标志,则被关联的地址是[shm_addr-(shm_addr%SHMLBA)]

SHMLBA的含义是“段低端边界地址倍数”,它必须是内存页面大小的整数倍。

SHM_RND的含义是圆整,即将共享内存被关联的地址向下圆整到离shm_addr最近的SHMLBA的整数倍地址处。

shmflg参数还支持:

SHM_RDONLY,进程仅能读取共享内存中的内容,若没有指定该标志则可对共享内存进行读写操作(这需要在创建时指定其读写权限)。

SHM_REMAP,如果地址shmaddr已经被关联到一段共享内存上,则重新关联。

SHM_EXEC,它指定对共享内存段的执行权限,对共享内存而言,执行权限实际上和读权限是一样的。

 

13.6.3  shmctl 系统调用

#include <sys/shm.h>

int shmctl ( int shm_id, int command, struct shmid_ds* buf ) ;

shm_id参数是由shmget调用返回的共享内存标识符。

cmmand参数指定要执行的命令。

shmctl支持的所有命令如表:


shmctl成功时的返回值取决于command参数,失败时返回 -1,并设置errno 

 

 

13.7  消息队列

消息队列是在两个进程间传递二进制块数据的一种简单有效的方式。

每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据。

13.7.1  msgget系统调用

#include <sys/msg.h>

int  msgget ( key_t  key , int  msgflg ) ;

key: 键值 。

msgflg:例如:IPC_CREAT | 0666

 

13.7.2  msgsnd 系统调用

#include <sys/msg.h>

int msgsnd ( int msqid , const void* msg_ptr,  size_t msg_sz,  int msgflg ) ;

msqid参数是由msgget调用返回的消息队列标识符。

msg_ptr参数指向一个准备发射的消息,消息模板被定义为如下类型:

struct msgbuf{

   long mtype ;  //消息类型

   char mtext ;   //消息数据

} ;

mtext是该消息的数据,并不一定就是char 类型,任意类型都可以的

msgflg参数控制msgsnd的行为,通常支持IPC_NOWAIT标志,即以非阻塞的方式发送消息。当发送消息时队列满了,默认情况下msgsnd将阻塞;若IPC_NOWAIT标志设置了,则msgsnd将立即返回并设置errnoEAGAIN 

处于阻塞状态的msgsnd调用可能被如下两种异常情况所中断:

1)消息队列被移除,此时msgsnd调用将立即返回并设置errnoEIDRM

2)程序接收到信号,此时msgsnd调用立即返回并设置errnoEINTR 

msgsnd成功时返回0,失败返回 -1 并设置errno msgsnd成功时将修改内核数据结构 。

 

13.7.3  msgrcv系统调用

msgrcv 系统调用从消息队列获取消息:

#include <sys/msg.h>

int msgrvc ( int msqid,  void* msg_ptr,  size_t  msg_sz, long int msgtype,  int msgflg ) ;

msqid参数是由msgget调用返回的消息队列标识符。

msg_ptr参数用于存储接收的消息,msg_sz参数指的是消息数据部分的长度。

msgtype参数指定接收何种类型的消息:

1)msgtype等于0,读取消息队列中的第一个消息。

2)msgtype大于0,读取消息队列中第一个类型为msgtype的消息(除非指定标志MSG_EXCEPT)。

3)msgtype小于0,读取消息队列中第一个类型值比msgtype的绝对值小的消息。

参数msgflg控制msgrcv函数的行为,它可以是如下一些标志的按位或:

1)IPC_NOWAIT:不阻塞

2)MSG_EXCEPT:如果msgtype大于0,则接收消息队列中第一个非msgtype类型的消息。

3)MSG_NOERROR:如果消息数据部分的长度超过了msg_sz,就将它截断。

处于阻塞状态的msgrcv调用可能被如下两种异常情况所中断:

1)消息队列被移除,此时msgrcv调用将立即返回并设置errnoEIDRM

2)程序接收到信号,此时msgrcv调用立即返回并设置errnoEINTR 

 

13.7.4  msgctl 系统调用

#include <sys/msg.h>

int msgctl ( int msqid, int command,  struct msqid_ds* buf) ;

msqid参数是由返回的共享内存的标识符。

command参数指定要执行的命令。

msgctl支持的命令如表:


 

msgctl成功时返回值取决于command参数,失败返回 -1并设置errno 

 

13.9  在进程间传递文件描述符

由于fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。

注意:传递一个文件描述符并不是传递一个文件描述符的值,而是要在接受进程中创建一个新的文件描述符并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项

如何在两个不相干的进程之间传递文件描述符呢?

Linux下,我们可以利用UNIXsocket在进程间传递特殊的辅助数据,以实现文件描述符的传递。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值