服务器编程入门(8)多进程编程

问题聚焦:
    进程是Linux操作系统环境的基础。
    本篇讨论以下几个内容,同时也是面试经常被问到的一些问题:
    1 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用
    2 僵尸进程
    3 进程间通信的方式之一:管道
    4 3种System V进程通信方式:信号量,消息队列和共享内存



fork系统调用
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. pid_t fork( void );  
函数说明:
  • 该函数每次调用返回两次
  • 在父进程中返回的是子进程的PID,在子进程中返回的是0,由此判断当前进程是父进程还是子进程(返回值是0的为子进程)
作用:
  • fork函数复制当前进程
  • 子进程的代码和父进程完全相同
  • 子进程复制父进程的大部分数据(堆数据,栈数据,静态数据)
  • 写时复制
  • 父进程中打开的文件描述符等在子进程中默认打开,因此它们的引用变量均+1.

Exec系列系统调用(6个)
在子进程中执行其他程序,即替换当前进程映像,需要使用exec系列函数
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <unistd.h>  
  2. extern char** environ;  
  3.   
  4. int execl( const char* path, const char* arg, ... );  
  5. int execlp( const char* file, const char* arg, ... );  
  6. int execle( cost char* path, const char* arg, ... , charconst envp[] );  
  7. int execv( const char* path, charconst argv[] );  
  8. int execvp( const char* file, charconst argv[] );  
  9. int execve( const char* path, charconst envp[] );  
参数说明:
path:指定可执行文件的完整路径
file:接受文件名,该文件的具体位置则在环境变量PATH中搜寻
arg:接收可变参数
argv:接收参数数组,它们都会被传递给新程序的main函数
envp:用于设置新程序的环境变量,如果未设置,则新程序使用由全局变量environ指定的环境变量

返回:
成功时,不返回;出错,返回-1,并设置errno
如果没出错,则源程序中exec调用之后的代码都不会执行,因为此时源程序已经被exec的参数指定的程序完全替换(包括代码和数据)

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



僵尸进程
僵尸进程:两种情况导致子进程导致僵尸态
  • 如果父进程需要查询子进程的退出状态,那么子进程结束后,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询。在子进程结束之后,父进程读取其退出状态之前,该子进程处于僵尸态。
  • 如果父进程退出或异常终止,而子进程继续运行,此时子进程的PPID(父进程PID)被设置成1,即init进程,即init进程接管了该进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
即:父进程没有正确处理子进程的返回信息,将导致子进程处于僵尸态,占据内核资源,造成内核资源的浪费。
下面介绍的函数,就是为了立即结束子进程的僵尸态。
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/types.h>  
  2. #include <sys/wait.h>  
  3. pid_t wait( int* stat_loc );  
  4. pid_t waitpid( pid_t pid, int* stat_loc, int options );  
函数说明:
wait函数:阻塞该进程,直到某个子进程结束运行为止。返回子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。
sys/wait.h头文件中定义了几个宏,解释子进程的退出状态信息


waitpid函数:等待由pid参数指定的子进程
当options参数设置为WHOHANG时,waitpid调用是非阻塞的:
如果pid指定的目标子进程还没结束或意外终止,则waitpid立即返回0;
如果目标子进程确实正常退出了,则waipid返回该子进程的PID
调用失败返回-1

Demo: 要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率
使用waitpid函数实现这一思想:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static void handle_child( int sig )  
  2. {  
  3.     pid_t pid;  
  4.     int stat;  
  5.     while ( ( pid = waitpid( -1, &stat, WHOHANG )) > 0 )  
  6.     {  
  7.         /* 对结束的子进程进行善后处理 */  
  8.     }  
  9. }  
看不太懂?貌似我们错过了某些重要的东西,主要是因为中间跳过了两章,直接看我感兴趣的多进程。。
我们先了解一下,当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,因此,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程。


管道pipe
作用:父进程和子进程间通信的常用手段,半双工模式
方式:fork系统调用后两个管道文件描述符(fd[0]和fd[1]都保持打开),通信时,父进程和子进程必须一个关闭fd[0],另一个关闭fd[1]
注意:socket编程接口提供了一个创建全双工管道的系统调用:socketpair

管道只能用于有关联的两个进程间的通信。

下面介绍3种System V IPC进程间通信方式

信号量
背景:当多个进程同时访问系统上的某个资源的时候,就需要考虑同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问
临界区:对资源访问的关键代码,可能引发竞态条件
信号量:确保临界区代码的独占式访问。
  • 一种特殊的变量
  • 只能取自然数值
  • 只支持两种操作:等待(P,要进入临界区)和信号(V,要退出临界区)
含义:假设有信号量SV,对它的P、V操作含义如下
  • P(SV),如果SV的值大于0,就将它减一,然后允许访问临界区;如果SV的值为0,则挂起进程的执行
  • V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加一
当信号量的值为1时,就是退化成互斥锁。如下图所示:

进程A和进程B在执行关键代码段(临界区)的前后,都会对SV进行PV操作,以确保临界区的独占式访问。

Linux信号量的API,三个系统调用:semget、semop和semctl,用于操作信号量集 。所以看起来会比简单的互斥锁要复杂一点。

semget系统调用
作用:创建一个新的信号量集,或者获取一个已经存在的信号量集
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/sem.h>  
  2. int semget ( key_t key, int num_sems, int sem_flags );  
参数说明:
key:一个键值,标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建\获取该信号量
num_sums:指定要创建/获取的信号量集中信号量的数据。如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为0
sem_flags:指定一组标志。
返回:成功时,返回一个正整数值,它是信号量集的标识符;semget失败时返回-1,并设置errno

semop系统调用
作用:改变信号量的值,即执行P\V操作
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/sem.h>  
  2. int semop ( int sem_id, struct sembuf* sem_ops, size_t num_sem_ops );  
参数说明:
sem_id:由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集
sem_ops:指向一个sembuf结构体类型的数组
struct sembuf:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct sembuf  
  2. {  
  3.     unsigned short int sem_num;    // 信号量集中信号量的编号  
  4.     short int sem_op;                        // 指定操作类型,可选正整数,0,负整数  
  5.     short int sem_flg;                        // 影响操作行为,可选值IPC_NOWAIT(无论信号量操作是否成功,都立即返回),SEM_UNDO(当进程退出时,取消正在进行的semop操作)  
  6. };  
关于sem_op和sem_flag字段如何影响semop系统调用的行为,当遇到时,请自行百度。
num_sem_ops:指定要执行的操作个数

semctl系统调用
作用:允许调用者对信号量进行直接控制
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/sem.h>  
  2. int semctl ( int sem_id, int sem_num, int command, ... );  
参数说明:
sem_id:由semget调用返回的信号量标识符
sem_num:指定被操作的信号量在信号量集中的编号
command:指定要执行的命令。
第4个参数由用户自定义,在sys/sem.h头文件中给出了它的推荐格式。

特殊键值IPC_PRIVATE
semget的key参数的特殊值,其值为0。
作用:
无论该信号量是否已经粗壮奶,semget都将创建一个新的信号量。
创建的信号量并非像它的名字那样是进程私有的,其他进程,尤其是子进程,也有方法来访问这个信号量。



共享内存
特点:
  • 不涉及进程之间的任何数据传输。
  • 必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。 
  • 因此,共享内存通常和其他进程间通信方式一起使用
Linux共享内存的API都定义在sys/shm.h,包括4个系统调用:shmget、shmat、shmdt、shmctl。

shmget系统调用
作用:创建一段新的共享内存,或者获取一段已经存在的共享内存
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/shm.h>  
  2. int shmget ( key_t key, size_t size, int shmflg );  
参数说明:
key:标识一段全局唯一的共享内存
size:指定共享内存的大小,单位是字节
shmflg:和semget系统调用的sem_flags参数相同,支持两个额外的标志:SHM_HUGETLB(使用大页面来为共享内存分配空间)和SHM_NORESERVE(不为共享内存保留交换分区)。
关联的内核数据结构:struct shmid_ds。

shmat和shmdt系统调用
使用共享内存需要注意的两个任务:
  • 共享内存在创建之后,不能立即使用,需要先将它关联到进程的地址空间中。——shmat
  • 使用完共享内存后,我们也需要把它从进程地址空间中分离。——shmdt
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/shm.h>  
  2. void shmat ( int shm_id, const void * shm_addr, int shmflg );  
  3. int shmdt ( const void* shm_addr );  
参数说明:
shm_id:共享内存标识符
shm_addr:指定要将共享内存关联到进程的哪块地址空间
shmflg:影响操作的最后效果,可选标志位SHM_RND。
关于shmflg支持的标志位,请用到时自行百度。

shmctl系统调用
作用:控制共享内存的某些属性
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/shm.h>  
  2. int shmctl ( int shm_id , int command, struct shmid_ds* buf );  
参数说明:
shm_id:共享内存标识符
command:要执行的命令。支持的命令如下图所示:
    
    
无关进程之间共享内存的方式
mmap可以实现无关进程之间的内存共享,需要文件支持。
shm_open无需文件支持。
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/mman.h>  
  2. #include <sys/stat.h>  
  3. #include <fnctl.h>  
  4. int shm_open ( const char* name, int oflag, mode_t mode );  
参数说明:
name:指定要创建/打开的共享内存对象
oflag:指定创建方式

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/mman.h>  
  2. #include <sys/stat.h>  
  3. #include <fnctl.h>  
  4. int shm_unlink ( const char *name );  
作用:将name参数指定的共享内存镀锡i昂标记为等待删除,当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。



消息队列
作用:两个进程之间传递二进制块数据的一种方式,简单有效。
特点:每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,不一定像管道和命名管道那样必须先进先出的方式接收数据。
相关的API定义在sys/msg.h中,包括四个系统调用:msgget、msgsnd、msgrcv、msgctl

msgget系统调用
作用:创建一个消息队列,或者获取一个已有的消息队列
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/msg.h>  
  2. int msgget ( key_t key, int msgflg );  
参数说明:
key:标识一个全局唯一的消息队列
msgflg:和semget系统调用的sem_flags参数相同
与内核数据结构msqid_ds相关联。

msgsnd系统调用:
作用:把一条消息添加到消息队列中
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/msg.h>;  
  2. int msgsnd ( int msgid, const void* msg_ptr, size_t msg_sz, int smgflg );  

msgrcv系统调用
作用:从消息队列中获取消息
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/msg.h>  
  2. int msgrcv ( int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg );  

msgctl系统调用
作用:控制消息队列的某些属性
定义:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <sys/msg.h>  
  2. int msgctl ( int msqid, int command, struct msqid_ds* buf );  



小结:
  • 三种System V IPC进程间通信方式都使用一个全局唯一的键值来描述一个共享资源。使用ipcs命令可以查看当前系统的共享资源实例。
  • 要尽可能缩短僵尸进程的存在时间
  • 简单了解一下fork和exec系系统调用



参考资料:
《Linux高性能服务器编程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值