文章目录
Linux多进程多线程编程
一、多进程编程
1、fork 函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 返回值:成功情况下述;失败返回-1并设置相应的errno
- fork创建的新进程称为子进程,此函数调用一次,但返回两次。 返回值的唯一区别是,子进程中的返回值是0,而父进程中的返回值是新子进程的进程ID。
- 子进程ID返回给父进程的原因是,一个进程可以有多个子进程,并且没有函数允许进程获取其子进程ID。
- fork返回0给子进程的原因是一个进程只能有一个父进程,并且子进程总是可以调用getppid来获取父进程的ID。 (进程ID 0是保留给内核使用的,所以0不可能是子进程ID。)
2、exec 系列函数
fork函数的一个用途是创建一个新进程(子程序),然后通过调用其中一个exec函数来执行另一个程序。当进程调用其中一个exec函数时,该进程将完全被新程序取代,新程序将开始在其主函数处执行。进程ID在执行过程中不会改变,因为没有创建新的进程;Exec只是用一个来自磁盘的全新程序替换当前进程(它的文本、数据、堆和堆栈段)。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execlp(const char *file, const char *arg0, ... /* (char *)0 */ );
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execv(const char *pathname, char *const argv[]);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// All seven return: −1 on error, no return on success
# 参数说明
pathname 指定可知行文件的完整路径,
file 中包含了1个或以上的'/',效果等同于pathname,否则在环境变量PATH中搜索
argv 接受参数数组,传递给被打开的新程序的main函数
envp参数用于设置新程序的环境变量,如果没设置,则使用由全局变量environ指定的环境变量
3、wait、waitpid 函数
这两个函数主要用于处理僵尸进程
僵尸进程介绍:多进程中,父进程一般需要跟踪子进程退出状态,因此当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程对子进程退出信息的查询,所以会造成两种形态的僵尸进程
- 1、子进程结束运行之后,父进程读取该子进程退出状态之前,这个子进程处于僵尸状态
- 2、父进程结束或者异常终止,而子进程继续运行,此时子进程的PPID将被操作系统设置为1,即init进程,init进程接管了它,等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸状态
僵尸进程会占据系统资源,这是高性能中不被允许的,所以用wait、waitpid 函数处理
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// Both return: process ID if OK, 0 (see later), or −1 on error
这两个函数的区别如下:
- 1、wait函数可以阻塞调用者直到子进程终止,而waitpid有一个选项可以阻止它阻塞。
- 2、waitpid函数不会等待第一个终止的子进程;它有许多选项来控制等待哪个进程
参数statloc
是一个指向整数的指针。如果该参数不是空指针,则终止进程的终止状态存储在该参数所指向的位置中。如果我们不关心终止状态,只需传递一个空指针作为这个参数。
下面几个宏帮助解释子进程的退出状态信息
宏 | 描述 |
---|---|
WIFEXITED(status) | 如果子进程正常终止,返回则为true。在这种情况下,我们可以执行WEXITSTATUS(status)获取子进程传递给exit、_exit或_exit的参数的低8位,也就是返回子进程的退出码 |
WIFSIGNALED(status) | 如果一个异常终止的子进程的状态被返回,通过接收到它没有捕捉到的信号,返回非0值。在这种情况下,我们可以执行WTERMSIG(状态)获取导致终止的信号值 |
WIFSTOPPED(status) | 如果子进程意外终止,返回一个非0值,此时可调用WSTOPSIG(status) 获取导致子进程停止的信号值 |
WIFCONTINUED(status) | 如果在任务控制停止后继续执行子任务,返回状态为true (XSI中;只有waitpid)。 |
wait的阻塞特性不适合服务程序,用waitpid解决这个问题。
waitpid只等待由pid参数指定的子进程
- 如果pid为-1,则和wait效果相同,等待任意一个子进程终止
- statloc参数和wait相同
- options参数可以控制waitpid函数行为,最常用的值是
WNOHANG
,当options是WNOHANG时,waitpit调用将是非阻塞的。如果pid指定子进程还没有结束或意外终止,waitpid立即返回0;如果目标子进程正常退出了,waitpid返回该子进程的PID。
对于waitpid函数而言,最好在某个子进程退出之后再调用它。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,父进程可以捕获SIGCHLD信号得知某个子进程退出,并在信号处理函数调用waitpid函数以“彻底结束”一个子进程。以下是处理代码
static void handle_child(int sig)
{
pid_t pid;
int stat;
while (pid = waitpid(-1, &stat, WNOHANG) > 0)
{
// 对结束的子进程进行善后处理
}
}
4、pipe 管道
管道是父进程和子进程间常用的通信手段,管道传递通信是单方向的,所以是半双工。父子进程间必须有一个关闭fd[0],另外一个进程关闭fd[1]
FIFO命名管道可以全双工,但是在网络编程中使用得不多
基本使用方法:
int main() {
int num = 0;
int fd[2];
pid_t pid;
char data[20] = {
};
if (pipe(fd) < 0)
{
printf("pipe error");
}
if ((pid = fork()) < 0)
{
printf("fork error");
}
else if (pid > 0) // parent
{
printf("\n\n\nI am parent\n");
close(fd[0]);
write(fd[1], "hello world\n", 12);
}
else // child
{
printf("\n\n\nI am child\n");
close(fd[1]);
num = read(fd[0], data, 20);
write(STDOUT_FILENO, data, num);
}
return 0;
}
5、信号量
信号量优劣:
- 1、信号量可以使线程进入休眠状态,会切换线程,信号量的开销要比自旋锁大,适用于占用资源比较久的场合。
- 2、信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
- 3、如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。
(2)如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。
当多个进程访问某个资源时,比如同时写入一个文件时,就需要考虑同步问题,确保在同一时刻只有一个进程对该资源独占式访问。
信号量是一种特殊的变量, 信号量有两种操作,常用P、V两个字母表示(荷兰语单词首字母),P为传递,V为释放,用S表示一个信号量
- P : 如果S的值大于0,就将S减1; 如果S的值为0,说明目标资源被占用中,则挂起当前进程
- V : 如果有其他进程因为操作S后被挂起,就唤醒那个进程;如果没有进程被挂起,就将S的值加1
下面操作一段关键代码举例(只是最简单的情况,实际上操作会复杂一些),假设已经创建了信号量设置值为1(这里将信号量理解为跨进程的全局变量),有进程A和进程B,进程A先访问关键代码段,穿过了P位置,进行了P操作,S值减1为0,如果进程A还没有走到V这个位置时,而进程B又走到了P点,因为S值为0,所以进程B会被挂起,直到进程A执行了V操作,B才会被唤醒继续访问关键代码段
Linux的信号量主要由3个函数操作:semget、semop、semctl
5.1、semget
用semget
创建一个新的信号量集,或者获得一个已有的信号量集
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// Returns: semaphore ID if OK, −1 on error
- key:标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的key值来创建/获取该信号量
- nsems: 指定要创建/获取的信号量集中信号量的数目。如果是创建新的信号量,该值必须指定;如果获取已经存在的信号量,则可以把它设置为0
- semflg : 指定一组标志,它低位的9个比特是该信号量的权限,它用来设置所有者、用户组、其他用户的权限。此外,它还可以和IPC_CREAT标志做按位“或”运算来创建新的信号量集。 还可以联合使用IPC_CREAT和IPC_EXCL标志确保创建一组新的唯一的信号量集。
IPC_CREAT | IPC_EXCL
和open函数的O_CREAT | O_EXCL
作用相似,若不存在该信号量,就创建一个新的;若信号量集已存在,semget()返回错误设置errno为EEXIST
如果key的值为IPC_PRIVATE,或者没有现有的信号量集与key相关联,并且在semflg中指定了IPC_CREAT,则会创建一组新的nsems信号量
当创建新的信号量集后,与之关联的内核数据结构体semid_ds
将被初始化
sem_otime设置为0
sem_ctime设置为当前时间
sem_nsems设置为nsems
struct semid_ds {
struct ipc_perm sem_perm; /* see below ipc_perm */
unsigned short sem_nsems; /* # of semaphores in set */
time_t sem_otime; /* last-semop() time */
time_t sem_ctime; /* last-change time */
...
};
IPC与每个IPC结构关联一个ipc_perm
结构体,这个结构定义了权限和所有者,至少包括以下成员:
struct ipc_perm {
uid_t uid; /* owner’s effective user ID */
gid_t gid; /* owner’s effective group ID */
uid_t cuid; /* creator’s effective user ID */
gid_t cgid; /* creator’s effective group ID */
mode_t mode; /* access modes */
...
};
在创建IPC结构时初始化所有字段; 之后,我们可以通过调用msgctl、semctl或shmctl来修改uid、gid和mode字段。 要更改这些值,调用进程必须是IPC结构的创建者或超级用户; 更改这些字段类似于为文件调用chown或chmod
5.2、semop
semop
函数改变信号量的值,即执行上面提及的P、V操作
信号量集中的每个信号量都有以下内核关联变量:
unsigned short semval; /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* PID of process that last */
semop对信号量操作实际就是对以上的内核变量进行操作。
#include <sys/sem.h>
int semop(int semid, struct *sembuf sops, size_t nsops);
// Returns: 0 if OK, −1 on error
- semid :由semget函数返回的信号量集ID,类似于操作句柄
- sops:指向一个
sembuf
结构体类型的数组,该结构体下面会描述 - nsops:指定要执行的操作个数,即sops数组中元素的个数。semop对数组sops中的每个成员按数组顺序依次操作,操作过程是原子操作
- 返回值: 0 if OK, −1 on error,失败时sops指定一切都不会被执行
struct sembuf {
unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */
short sem_op; /* operation (negative, 0, or positive) */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
};
1、sem_num
表示信号集中信号量的编号,0表示信号集中的第一个信号量
2、sem_op
指定操作类型负数、0、正数。而每种类型的操作又受到sem_flg
的成员的影响
3、sem_op和sem_flg按照如下方式影响semop的操作
- sem_op > 0 : 则
semop
将该信号量的值(semval)增加sem_op,调用进程对该信号量需要拥有写权限 - sem_op = 0 : 则表示调用进程希望等待信号量的值变为0,调用进程对该信号量需要拥有读权限。
如果此时信号量的值是0,则立即成功返回。
如果此时信号量的值非0,情况如下:- 如果指定了IPC_NOWAIT,
semop
返回一个EAGAIN错误 - 如果没有指定IPC_NOWAIT,这个信号量的semzcnt值就会增加1(因为调用者即将进入睡眠状态),并且调用进程将被挂起,直到发生以下三种情况之一:
- 信号量的值(semval)变为0,此时系统将该信号量的semzcnt值减1(因为调用进程已经完成了等待)
- 信号量从系统中移除,在这种情况下,
semop
函数返回一个EIDRM错误 - 调用被捕获的信号中断,此时信号量的semzcnt值减1(因为调用进程不再等待),并且
semop
函数调用失败返回一个EINTR错误
- 如果指定了IPC_NOWAIT,
- sem_op < 0 :表示对信号量进行减操作,即希望获得该信号量控制的资源。
- 如果信号量的值(semval)大于或等于sem_op的绝对值(资源可用),则从信号量的值(semval)中减去sem_op的绝对值,semop操作成功,调用进程立即获得信号量。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中。
- 如果信号量的值(semval)小于sem_op的绝对值,情况如下
- 如果指定了IPC_NOWAIT,
semop
返回一个EAGAIN错误 - 如果没有指定IPC_NOWAIT,这个信号量的semncnt值就会增加(因为调用者即将进入睡眠状态),并且调用进程被挂起,直到发生以下三种情况之一:
- 信号量的值(semval)大于或等于sem_op的绝对值(即其他进程V释放过了资源),这个信号量的semncnt的值减1(因为调用进程已经完成了等待),并且信号量的值(semval)会减去sem_op的绝对值。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中
- 信号量从系统中移除,在这种情况下,
semop
函数返回一个EIDRM错误 - 调用被捕获的信号中断,此时信号量的semncnt值减1(因为调用进程不再等待),并且
semop
函数调用失败返回一个EINTR错误
- 如果指定了IPC_NOWAIT,
5.3、semctl
semctl函数可以对信号量进行直接控制
int semctl(int semid, int semnum, int cmd, ...);
- semid :由semget函数返回的信号量集ID,类似于操作句柄
- semnum :指定被操作的信号量在信号量集中的编号
- cmd : 指定要执行的命令
- semun : 第四个参数由用户自定义,可选参数是实际的联合体,而不是指向联合体的指针,sys/sem.h文件给出了推荐格式,如下
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
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次数(带SEM_UNDO标志的semop操作的次数) */
};
cmd 执行的参数和解释如下图:
5.4、信号量同步父子进程
这里使用IPC_PRIVATE信号量来同步父子进程。
假设现在有两个人,因为只有一张小床,所以同一时间只能有一个人在睡觉。下面代码实现了这个过程,但要注意的是最后这个信号量会被删除两次(父子进程各一次,最后一次就会报错)
#include <stdio.h>
#include <wait.h>
#include <sys/sem.h>
#include <unistd.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */
};
// op为-1时执行P操作,为1时执行V操作
void pv(int sem_id, int op) {
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = op;
sem_buf.sem_flg = SEM_UNDO;
// 第3个参数是要操作的元素个数,这里是1
semop(sem_id, &sem_buf, 1);
}
int main() {
int sem_id = semget(IPC_PRIVATE, 1, 0666);
union semun sem_un;
sem_un.val = 1;
// 设置信号量的值semval为sem_un.val=1
semctl(sem_id, 0, SETVAL, sem_un);
pid_t pid = fork();
if (pid < 0) {
return 1;
}
else if (pid == 0) {
// 子进程
printf("子进程尝试获取二进制信号量\n");
// 传递信号量P
pv(sem_id, -1);
printf("子进程睡觉中……\n");
sleep(5);
printf("子进程醒来了!\n");
// 释放信号量V
pv(sem_id, 1);
}
else {
// 父进程
printf("*父进程尝试获取二进制信号量\n");
// 传递信号量P
pv(sem_id, -1);
printf("*父进程睡觉中……\n");
sleep(5);
printf("*父进程醒来了!\n");
// 释放信号量V
pv(sem_id, 1);
}
// 以下代码也很关键,等待子进程终止
waitpid(pid, NULL, 0);
// 删除信号量,唤醒等待信号量的进程
semctl(sem_id, 0, IPC_RMID, sem_un);
return 0;
}
运行程序后就理解整个过程了,打印结果如下:
*父进程尝试获取二进制信号量
*父进程睡觉中……
子进程尝试获取二进制信号量
*父进程醒来了!
子进程睡觉中……
子进程醒来了!
5.5、信号量同步任意两个进程
为了方便将信号量操作的函数集成在一个sem_head.h
头文件中,这种跨进程的信号量我们需要事先约定好key值
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
// 信号量的key
#define SEM_KEY 0x993A
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct