Linux高性能服务器编程——第13章 多进程编程

tech2023-08-03  97

Linux多进程编程包含内容:

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

13.1 fork系统调用

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

每次调用返回两次;父进程返回子进程ID,子进程返回0; fork调用在内核进程表创建一个新的进程表项。进程表项中的堆指针、栈指针和标志寄存器的值与父进程相同。子进程的PPID设置成原进程的PID,原进程的信号处理函数对子进程不起作用。 子进程的数据复制采用写时复制,即只有任一进程对数据执行写操作时,复制才会发生(操作系统给子进程分配内存并复制父进程的数据)。

13.2 exec系列系统调用

需要在子进程种执行其他程序,即替换当前进程映像,需要使用exec系列函数;

#include <unist.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中搜寻; envp:设置新程序的环境变量,未设置则新程序使用environ指定的环境变量; 一般情况,exec函数不返回,出错返回-1;原程序被exec参数指定的程序完全替换; exec不会关闭原程序打开的文件描述符,除非设置了类似SOCK_CLOEXEC属性;

13.3 处理僵尸进程

父进程需要跟踪子进程的退出状态。当子进程结束时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询。子进程结束运行后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。另外一种使子进程进入僵尸态的情况:父进程结束或异常终止,而子进程继续运行。此时子进程的PPID设置为1,即init进程。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参数指向的内存中。

waitpid只等待由pid参数指定的子进程,如果pid取-1,则与wait相同,即等待任一子进程结束。stat_loc同上,options参数控制waitpid函数行为。取值为WNOHANG,waitpid调用为非阻塞;如果pid指定的目标子进程还未结束或意外终止,waitpid调用时立即返回0; 要在事件已经发生的情况下执行非阻塞调用才能提高程序效率。对于waitpid函数,最好在某个子进程退出在调用它。在一个进程结束后,它将给其父进程发送SIGCHLD信号。父进程中捕捉SIGCHLD信号,在信号处理函数中调用waitpid彻底结束一个子进程。

static void handle_child(int sig) { pid_t pid; int stat; while( (pid = waitpid(-1, &stat, WNOHANG) ) > 0) { }

13.4 管道

创建全双工管道的系统调用:socketpair 管道只能用于有关联的两个进程(比如父、子进程)间的通信。 三种System V IPC(信号量、消息队列和共享内存)用于无关联的多个进程之间的通信。使用一个全局唯一的键值来标识一条信道。

13.5 信号量

13.5.1 信号量原语

程序对共享资源的访问代码,称为临界区;对进程同步,确保任一时刻只有一个进程能进入关键代码段。 信号量(Semaphore)只能取自然数值并只支持两种操作:等待(wait)和信号(signal)。P操作进入临界区,V操作退出临界区。 假设有信号量SV,对其进行P、V操作含义:

P(SV):如果SV大于0,将其减1;SV为0,挂起进程执行;V(SV):其他进程因为等待SV被挂起,则唤醒之;没有,将SV加1; 信号量取值:二进制信号量; Linux信号量的API定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop和semctl。操作一组信号量,而不是单个信号量。

13.5.2 semget系统调用

创建一个新的信号量集,或者获取一个已经存在的信号量集。

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

key是一个键值,标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建/获取信号量。 num_sems参数指定要创建/获得的信号量集中信号量的数目。创建信号量,该值必须被指定;获得已经存在的信号量,将其设置为0; sem_flags指定一组标志。 **返回值:**返回一个正整数值,是信号量集的标识符;

13.5.3 semop系统调用

改变信号量的值,即执行P、V操作。 与信号量关联的重要内核变量;

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_NOWAIT和SEM_UNDO。 IPC_NOWAIT类似非阻塞I/O; SEM_UNDO,当进程退出取消正在进行的semop操作;

sem_op和sem_flg影响semop行为:

sem_op大于0,semop将被操作的信号量的值semval增加sem_op。sem_op等于0,表示这是一个“等待0”操作。如果此时信号量的值为0,调用立即成功返回。信号量的值不为0,semop失败返回或者阻塞进程以等待信号量变0;sem_op小于0,表示对信号量值进行减操作,期望获得信号量。

num_sem_ops指定要执行的操作个数,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指定执行的命令;

13.5.5 特殊键值IPC_PRIVATE

semget调用可给其key传递一个特殊键值IPC_PRIVATE,这样无论该信号量是否存在,semget都将创建新的信号量。

13.6 共享内存

最高效的IPC机制,不涉及进程之间的任何数据传输。必须使用其他辅助手段来同步对共享内存的访问,否则会产生竟态条件。共享内存的API定义在sys/shm.h中,包含四个系统调用:shmget、shmat、shmdt和shmctl。

13.6.1 shmget系统调用

创建一段新的共享内存,获取一段已经存在的内存。

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

key:标识一段全局唯一的共享内存; size:指定共享内存的大小,单位是字节(创建新的共享内存必须指定),获得已存在的设为0; shmflg标志位; shmget成功返回正整数值,是共享内存的标识符。 shmget创建共享内存,这段共享内存所有字节都初始化为0,与之关联的内核数据结构shmid_ds被创建并初始化,

struct shmid_ds { struct ipc_perm shm_perm; //共享内存的操作权限 size_t shm_segsz; //共享内存大小 __time_t shm_atime; //最后一次调用shmat的时间 __time_t shm_dtime; //最后一次调用shmdt的时间 __time_t shm_ctime; //最后一次调用shmctl的时间 __pid_t shm_cpid; //创建者的PID __pid_t shm_lpid; //最后一次执行shmat或shmdt操作的进程的PID shmatt_t shm_nattach; //目前关联到此共享内存的进程数量

13.6.2 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的影响; shmflg标志:

SHM_RDONLY,只读标志;SHM_REMAP,若shmaddr已被关联共享内存,则重新关联。SHM_EXEC,对共享内存段的执行权限。 shmat成功返回共享内训关联到的地址; shmdt将关联到shm_addr的共享内存从进程中分离。

13.6.3 shmctl系统调用

shmctl控制共享内存的某些属性。

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

shm_id由shmget调用返回的额共享内存标识符;

13.6.4 共享内存的POSIX方法

利用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:O_RDONLY | O_RDWR(可读可写) | O_CREAT(如果共享内存对象不存在,则创建之。)| O_EXCL(和 O_CREAT一起使用,内存对象存在shm_open调用返回错误,否则创建一个新的共享内存对象。)| O_TRUNC(共享对象存在,则把它截断,使其长度为0。) shm_open成功返回一个文件描述符。可用于后续mmap调用,将共享内存关联到调用进程。 由shm_open创建的共享内存对象使用完也需被删除。

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

将name指定的共享内存标记为等待删除。

13.7 消息队列

是在两个进程之间传递二进制块数据的简单有效的方式。每个数据块都有一个特定的类型。 消息队列的API定义在sys/msg.h头文件,包含4个系统调用:msgget、msgsnd、msgrcv和msgctl。

13.7.1 msgget系统调用

msgget创建一个消息队列,或者获得一个已有的消息队列。

#include <sys/msg.h> int msgget(key_t key, int msgflg);

key标识一个全局唯一的消息队列; 成功返回一个正整数值,是消息队列的标识符。

13.7.2 msgsnd系统调用

把一条消息添加到消息队列中。

#include <sys/msg.h> int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);

msgqid是由msgget调用返回的消息队列标识符; msg_ptr指向一个准备发送的消息,消息类型:

struct msgbuf { long mtype; char mtext[512]; }

msg_sz是消息队列的数据部分(mtext)的长度; msgflg控制msgsnd的行为,仅支持IPC_NOWAIT(非阻塞) 处于阻塞状态的msgsnd可被两种异常中断:

消息队列被移除。errno设置为EIDRM;程序接收到信号。errno为EINTR;

13.7.3 msgrcv系统调用

从消息队列中获取消息。

#include <sys/msg.h> int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

msqid是msgget调用返回的消息队列标识符; msg_ptr存储接收的消息; msg_sz指的是消息数据部分的长度; msgtype消息类型:

等于0;读取消息队列第一个消息;大于0;读取消息队列第一个类型为msgtype的消息;小于0;读取第一个类型值比msgtype的绝对值小的消息;

13.7.4 msgctl系统调用

控制消息队列的某些属性。

#include <sys/msg.h> int msgctl(int msqid, int command, struct msqid_ds* buf);

msgqid:共享内存标识符;

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

传递一个文件描述符并不是传递一个文件描述符的值,而是在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核的相同的文件表项。 如何将子进程中打开的文件描述符传递给父进程(如何在两个不相干的进程之间传递文件描述符)。使用UNIX域中的socket在进程间传递特殊的辅助数据,以实现文件描述符的传递。

最新回复(0)