Linux高性能服务器编程---多进程编程

内容:

  • 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用
  • 僵尸进程以及如何避免僵尸进程
  • 进程间通信(Inter-Process Communication, IPC)最简单的方式:管道
  • 3种System V进程间通信方式:信号量、消息队列和共享内存。它们都是由 AT&T System V2版本的UNIX引入的,所以统称为System V IPC
  • 在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据

fork系统调用

#include <sys/types.h>
#include <unistd.h>
pid_t fork( void );

该函数的每次调用都会返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0。所以可以利用返回值来判断是子进程还是父进程,fork调用失败时,返回-1,并设置errno。

fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但该进程的PPID被设置成原进程的PID,信号位图被清除(元进程设置的信号处理函数不再对新进程起作用)。

子进程的代码和父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是写时复制(copy on write),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据

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

 

exec系列系统调用

有时需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用如下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则接受参数数组,它们都会被传递给新程序(path或file指定的程序)的main函数。envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。

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

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

 

处理僵尸进程

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

              case2:父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。init进程接管了该子进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。

如果父进程没有正确地处理子进程地返回信息,子进程都将停留在僵尸态,并占用内核资源

下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid( pid_t pid, nt* stat_loc, int options );

wait 函数将阻塞进程,直到该进程的某个子进程结束运行位置。它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。sys/wait.h头文件中定义了几个宏来帮助解释子进程的退出状态信息。

子进程状态信息
含义
WIFEXITED(stat_val)如果子进程正常结束,它就返回一个非0值
WEXITSTATUS(stat_val)如果WIFEXITED非0,它返回子进程的退出码
WIFSIGNALED(stat_val)如果子进程是因为一个未捕获的信号而终止,它就返回一个非0值
WTERMSIG(stat_val)如果WIFSIGNALED非0,它返回一个信号量
WIFSTOPPED(stat_val)如果子进程意外终止,它就返回一个非0值
WSTOPSIG(stat_val)如果WIFSTOPPED非0,它返回一个信号量

waitpid只等待由pid参数指定指定的子进程,如果pid取值为-1,那么它就和wait函数相同,即等待任意一个子进程结束。stat_loc参数的含义和wait函数的stat_loc参数相同。options参数可以控制waitpid函数的行为。该参数最常用的取值是WNOHANG。当options的取值是WNOHANG时,waitpid调用将是非阻塞的;如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正常退出了,则waitpid返回该子进程的PID。waitpid调用失败时返回-1并设置errno。

要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。对waitpid函数而言,我们最好在某个子进程退出之后再调用它。我们通过SIGCHLD信号得知某个子进程已经退出,我们在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程。

 

管道

pipe除了可以用于进程于进程之间通信外,还可以用于父进程与子进程间的通信。

管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0] 和 fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须都有一个关闭fd[0] ,另一个关闭fd[1]。比如,通过管道实现从父进程向子进程写数据。

如果要实现父、子进程之间的双向数据传输传输,就必须使用两个管道。socket编程接口提供了一个创建全双工管道的系统调用:socketpair。

 

信号量 P\V

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

Linux的信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop 和 semctl。它们都被设计为操作一组信号量,即信号量集,而不是单个信号量。

 

semget系统调用

semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。其定义如下:

#include <sys/sem.h>
int semget( key_t key, int num_sems, int sem_flags );

key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一地标识一个文件一样。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。

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

sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。

 

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

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

#include <sys/sem.h>
/* 该结构体用于描述IPC对象(信号量、共享内存和消息队列) */
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的时间
                                 // 省略其他填充字段
}


semget对semid_ds结构体的初始化包括:
1、将sem_perm.cuid和sem_perm.uid设置为调用进程的有效用户ID

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

3、将sem_perm.mode的最低9位设置为sem_flags参数的最低9位

4、将sem_nsems设置位num_sems。

5、将sem_otime设置为0

6、将sem_ctime设置为当前的系统时间

 

semop系统调用

semop系统调用改变信号量的值,即执行P、V操作。其相关的内核变量如下:

unsigned short semval;    // 信号量的值
unsigned short semzcnt;    // 等待信号量变为0的进程数量
unsigned short semncnt;    // 等待信号量值增加的进程数量
pid_t sempid;               // 最后一次执行semop操作的进程ID

semop对信号量的操作实际上就是对这些内核变量的操作。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结构体类型的数组,sembuf结构体的定义如下:

struct sembuf
{
    unsigned short int sem_num;
    short int sem_op;
    short int sem_flg;
}

...

semctl系统调用

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

#include <sys/sem.h>
int semctl( int sem_id, int sem_num, int command, ... );

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

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

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

 

共享内存

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

Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统效用:shmget、shmat、shmdt、和 shmctl

 

shmget系统调用

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

#include <sys/shm.h>
int shmget( key_t key, size_t size, int shmflg );

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

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

shmflg参数的使用和含义与semget系统调用的sem_flags参数相同。不过shmflag支持两个额外的标志:

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

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

shmget成功时返回一个正整数值,它是共享内存的标识符。shmget失败时返回-1,并设置errno。

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

 

shmat和shmdt系统调用

共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,还需要将它从进程地址空间中分离。

#include <sys/shm.h>
void* shmat( int shm_id, const void* shm_addr, int shmflg );
int shmdt( const void* shm_addr );

shm_id参数由shmget调用返回的共享内存标识符。shm_addr参数指定将共享内存关联到进程的哪块地址空间,最终的效果还受到shmflg参数的可选标志SHM_RND的影响。

 

shmctl系统调用

shmctl系统调用控制共享内存的某些属性。

#include <sys/shm.h>
int shmctl( int shm_id, int command, struct shmid_ds* buf );

其中,shm_id参数是由shmget调用返回的共享内存标识符。command参数指定要执行的命令。

shmctl支持的命令可以通过 man shmctl 查看

 

共享内存的POSIX方法

Linux提供另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open( const char* name, int oflag, mode_t mode );

name参数指定要创建/打开的共享内存对象。

oflag参数指定创建方式 (只读、可读可写、不存在则创建)

shm_open调用成功时返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。

删除共享内存的系统调用如下:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_unlink( const char *name );

该函数将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值