高并发reactor服务器[中]
四、进程控制和进程同步
1.信号
1.1 信号的基本概念
信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但不能给进程传递任何数据。
信号产生的原因有很多,在Shell中,可以用kill
和killall
命令发送信号:
kill -信号的类型 进程编号 killall -信号的类型 进程名
1.2 信号的类型
信号名 | 信号值 | 默认处理动作 | 发出信号的原因 |
---|---|---|---|
SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
SIGINT | 2 | A | 键盘中断 Ctrl+c |
SIGQUIT | 3 | C | 键盘的退出键被按下 |
SIGILL | 4 | C | 非法指令 |
SIGTRAP | 5 | C | 断点指令 |
SIGABRT | 6 | C | 由abort(3)发出的中止信号 |
SIGBUS | 7 | C | 总线错误 |
SIGFPE | 8 | C | 浮点异常 |
SIGKILL | 9 | A | kill -9 杀死进程,该信号不能被捕获和忽略 |
SIGUSR1 | 10 | A | 用户定义信号1 |
SIGSEGV | 11 | C | 无效的内存引用(数组越界、操作空指针) |
SIGUSR2 | 12 | A | 用户定义信号2 |
SIGPIPE | 13 | A | 向一个无读进程的管道写数据 |
SIGALRM | 14 | A | 闹钟信号,由alarm()函数发出的信号 |
SIGTERM | 15 | A | 终止信号,默认发送的信号 |
SIGSTKFLT | 16 | A | 栈错误 |
SIGCHLD | 17 | B | 子进程结束时发出 |
SIGCONT | 18 | D | 继续执行已经停止的进程 |
SIGSTOP | 19 | D | 停止进程 |
SIGTSTP | 20 | D | 终端按下停止键 |
SIGTTIN | 21 | D | 后台进程请求读终端 |
SIGTTOU | 22 | D | 后台进程请求写终端 |
SIGURG | 23 | B | 紧急条件检测(套接字) |
SIGXCPU | 24 | C | 超出CPU时间限制 |
SIGXFSZ | 25 | C | 超出文件大小限制 |
SIGVTALRM | 26 | A | 虚拟时钟信号 |
SIGPROF | 27 | A | 分析时钟信号 |
SIGWINCH | 28 | B | 窗口大小变化 |
SIGPOLL | 29 | B | 轮询(Sys V) |
SIGPWR | 30 | A | 电源故障 |
SIGSYS | 31 | C | 非法系统调用 |
A的缺省动作是终止进程。
B的缺省动作是忽略此信号。
C的缺省动作是终止进程并进行内核映像转储。
D的缺省动作是停止进程,进入停止状态的程序还能重新继续执行。
1.3 信号的处理
进程对信号的处理方法有三种:
- 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
- 设置中断的处理函数,收到信号后,由该函数来处理。
- 忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal()
函数可以设置程序对信号的处理方式。
函数声明:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
参数说明:
sig
:指定要捕获的信号。func
:指向信号处理函数的指针。处理函数需要接收一个整型参数,这个参数是捕获的信号编号。
SIG_DFL
:SIG_DFL宏表示默认的信号处理方式。使用SIG_DFL
作为signal
函数的第二个参数时,表示对该信号采用系统默认的处理方式。SIG_IGN
:SIG_IGN宏表示忽略信号。使用SIG_IGN
作为signal
函数的第二个参数时,表示进程在接收到该信号时将忽略它,不进行任何处理。这在某些情况下可以防止进程被意外终止或中断。SIG_ERR
:SIG_ERR
宏用于指示错误。它并不是作为signal
函数的第二个参数来使用,而是作为signal
函数的返回值来表示调用失败。如果signal
函数的调用失败,它将返回SIG_ERR
。这通常用于检测和处理signal
函数调用中的错误。
1.4 信号有什么用
服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为进程被杀的时候,是突然死亡,没有安排善后工作。
如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。
向服务程序发送 0 的信号,可以检测程序是否存活。
1.5 发送信号
Linux操作系统提供了 kill
和 killall
命令向程序发送信号,在程序中,可以用 kill()
库函数向其它进程发送信号。
函数声明:
int kill(pid_t pid, int sig);
kill()
函数将参数 sig
指定的信号传给参数 pid
指定的进程。
参数 pid
有几种情况:
pid > 0
将信号传给进程为pid
的进程。pid = 0
将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意这个行为依赖于系统实现。pid < -1
将信号传给进程组ID为|pid|
的所有进程。pid = -1
将信号传给所有有权限发送信号的进程,但不包括发送信号的进程。
2.进程终止
有8种方式可以中止进程,其中5种为正常终止,它们是:
- 在
main()
函数用return
返回; - 在任意函数中调用
exit()
函数; - 在任意函数中调用
_exit()
或_Exit()
函数; - 最后一个线程从其启动例程(线程主函数)用
return
返回; - 在最后一个线程中调用
pthread_exit()
返回;
异常终止有3种方式,它们是:
- 调用
abort()
函数中止; - 接收到一个信号;
- 最后一个线程对取消请求做出响应。
2.1 进程终止的状态
在 main()
函数中,return
返回的值即终止状态,如果没有 return
语句或调用 exit()
,那么该进程的终止状态是0。
在Shell中,查看进程终止的状态:
echo $?
正常终止进程的3个函数(exit()
和 _Exit()
是由 ISO C 说明的,_exit()
是由 POSIX 说明的):
void exit(int status); void _exit(int status); void _Exit(int status);
status
进程终止的状态。
2.2 资源释放问题
return
表示函数返回,会调用局部对象的析构函数,main()
函数中的return
还会调用全局对象的析构函数。exit()
表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。_exit()
和_Exit()
直接退出,不会执行清理工作。
2.3 进程的终止函数
进程可以用 atexit()
函数登记终止函数(最多32个),这些函数将由 exit()
自动调用。
int atexit(void (*function)(void));
exit()
调用终止函数的顺序与登记时相反。
3.调用可执行程序
3.1 system() 函数
system()
函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()
函数就行了。
函数声明:
int system(const char * string);
system()
函数的返回值比较麻烦。
- 如果执行的程序不存在,
system()
函数返回非0; - 如果执行程序成功,并且被执行的程序终止状态是0,
system()
函数返回0; - 如果执行程序成功,并且被执行的程序终止状态不是0,
system()
函数返回非0。
3.2 exec 函数族
exec
函数族提供了另一种在进程中调用程序(二进制文件或Shell脚本)的方法。
exec
函数族的声明如下:
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 execvpe(const char *file, char *const argv[], char *const envp[]);
注意:
- 如果执行程序失败则直接返回-1,失败原因存于
errno
中。 - 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
- 如果执行成功则函数不会返回,当在主程序中成功调用
exec
后,被调用的程序将取代调用者程序,也就是说,exec
函数之后的代码都不会被执行。 - 在实际开发中,最常用的是
execl()
和execv()
,其他的极少使用。
4.创建进程
4.1 Linux的0、1和2号进程
整个Linux系统全部的进程是一个树形结构。
- **0号进程(系统进程)**是所有进程的祖先,它创建了1号和2号进程。
- **1号进程(systemd)**负责执行内核的初始化工作和进行系统配置。
- **2号进程(kthreadd)**负责所有内核线程的调度和管理。
用pstree
命令可以查看进程树:
pstree -p 进程编号
4.2 进程标识
每个进程都有一个非负整数表示的唯一的进程ID。虽然是唯一的,但是进程ID可以复用。当一个进程终止后,其进程ID就成了复用的候选者。Linux采用延迟复用算法,让新建进程的ID不同于最近终止的进程所使用的ID。这样防止了新进程被误认为是使用了同一个ID的某个已终止的进程。
获取进程ID的函数:
pid_t getpid(void); // 获取当前进程的ID。 pid_t getppid(void); // 获取父进程的ID。
4.3 fork()函数
一个现有的进程可以调用fork()
函数创建一个新的进程。
函数声明:
pid_t fork(void);
由fork()
创建的新进程被称为子进程。
fork()
函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。
子进程和父进程继续执行fork()
之后的代码,子进程是父进程的副本。子进程拥有父进程数据空间、堆和栈的副本(注意:子进程拥有的是副本,不是和父进程共享)。
fork()
之后,父进程和子进程的执行顺序是不确定的。
4.4 fork()的两种用法
- 父进程希望复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用
fork()
,让子进程处理这些请求,而父进程则继续等待下一个连接请求。 - 进程要执行另一个程序。这种用法在Shell中很常见,子进程从
fork()
返回后立即调用exec
。
4.5 共享文件
fork()
的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。
此时可以看见只有十万行数据。
此时应该是二十万行数据,少一行可能是文件写入操作并不是原子的,在没有同步机制的情况下,两个进程可能在同一时间尝试写入文件的不同部分,导致写入的数据相互干扰。
4.6 vfork()函数
vfork()
函数的调用和返回值与fork()
相同,但两者的语义不同。
vfork()
函数用于创建一个新进程,而该新进程的目的是exec
一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec
,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。
vfork()
和fork()
的另一个区别是:vfork()
保证子进程先运行,在子进程调用exec
或exit
之后父进程才恢复运行。
5.僵尸进程
在操作系统中,僵尸进程(Zombie Process)是指已经终止但其父进程尚未读取其退出状态的子进程。僵尸进程虽然不再运行,但仍然占据进程表中的一个条目,以便内核能保存该进程的退出状态信息(如进程ID、退出状态等),直到父进程读取这些信息。
5.1 造成僵尸进程的原因
如果父进程比子进程先退出,子进程将由1号进程托管(这也是一种让进程在后台运行的方法)。
如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程。
5.2 僵尸进程的危害
内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构。父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因没有可用的进程编号而导致系统不能产生新的进程。
5.3 避免僵尸进程的方法
- 处理SIGCHLD信号:当子进程退出时,内核会向父进程发送SIGCHLD信号。如果父进程用
signal(SIGCHLD, SIG_IGN)
通知内核表示自己对子进程的退出不感兴趣,那么子进程退出后会立刻释放其数据结构。 - 使用
wait()
/waitpid()
函数:父进程通过调用这些函数等待子进程结束,并获取其退出状态,从而释放子进程占用的资源。
pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc, int options); pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
返回值是子进程的编号。
stat_loc
是子进程终止的信息:
a) 如果是正常终止,宏 WIFEXITED(stat_loc)
返回真,宏 WEXITSTATUS(stat_loc)
可获取终止状态;
b) 如果是异常终止,宏 WTERMSIG(stat_loc)
可获取终止进程的信号。
如果父进程很忙,可以捕获 SIGCHLD
信号,在信号处理函数中调用 wait()
/waitpid()
。
6.多进程与信号
[进程间发送信号](##1.5 发送信号)
在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出。
如果父进程收到退出信号,应该向全部子进程发出退出信号,然后自己退出。
7.共享内存
多线程共享进程的地址空间,如果多个线程需要访问同一块内存,用全局变量就可以了。
在多进程中,每个进程的地址空间是独立的,不共享的,如果多个进程需要访问同一块内存,不能用全局变量,只能用共享内存。
共享内存(Shared Memory)允许多个进程(不要求进程之间有血缘关系)访问同一个内存空间,是多个进程之间共享和传递数据最有效的方式。进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其他的进程读到的数据也将会改变。
共享内存并未提供锁机制,也就是说,在某个进程对共享内存进行读/写的时候,不会阻止其它进程对它的读/写。如果要对共享内存的读/写加锁,可以使用信号量。 Linux中提供了一组函数用于操作共享内存。
7.1 shmget函数
该函数用于创建/获取共享内存。
int shmget(key_t key, size_t size, int shmflg);
- key 共享内存的键值,是一个整数(
typedef unsigned int key_t
),一般采用十六进制,例如0x5005
,不同共享内存的key不能相同。 - size 共享内存的大小,以字节为单位。
- shmflg 共享内存的访问权限,与文件的权限一样,例如
0666|IPC_CREAT
表示如果共享内存不存在,就创建它。 - 返回值:成功返回共享内存的id(一个大于0的整数),失败返回-1(系统内存不足,没有权限)。
用 ipcs -m
可以查看系统的共享内存,包括:键值(key),共享内存id(shmid),拥有者(owner),权限(perms),大小(bytes)。
用 ipcrm -m 共享内存id
可以手动删除共享内存,如下:
注意:共享内存中的数据类型不能使用容器,只能用基本数据类型。
7.2 shmat函数
该函数用于把共享内存连接到当前进程的地址空间。
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid 由
shmget()
函数返回的共享内存标识。 - shmaddr 指定共享内存连接到当前进程中的地址位置,通常填0,表示让系统来选择共享内存的地址。
- shmflg 标志位,通常填0。
调用成功时返回共享内存起始地址,失败时返回 (void *)-1
。
7.3 shmdt函数
该函数用于将共享内存从当前进程中分离,相当于 shmat()
函数的反操作。
int shmdt(const void *shmaddr);
- shmaddr
shmat()
函数返回的地址。
调用成功时返回0,失败时返回-1。
7.4 shmctl函数
该函数用于操作共享内存,最常用的操作是删除共享内存。
int shmctl(int shmid, int command, struct shmid_ds *buf);
- shmid
shmget()
函数返回的共享内存id。 - command 操作共享内存的指令,如果要删除共享内存,填
IPC_RMID
。 - buf 操作共享内存的数据结构的地址,如果要删除共享内存,填0。
调用成功时返回0,失败时返回-1。
注意,用 root
创建的共享内存,不管创建的权限是什么,普通用户无法删除。
7.5 循环队列
7.6 基于共享内存的循环队列
调用成功时返回0,失败时返回-1。
7.4 shmctl函数
该函数用于操作共享内存,最常用的操作是删除共享内存。
int shmctl(int shmid, int command, struct shmid_ds *buf);
- shmid
shmget()
函数返回的共享内存id。 - command 操作共享内存的指令,如果要删除共享内存,填
IPC_RMID
。 - buf 操作共享内存的数据结构的地址,如果要删除共享内存,填0。
调用成功时返回0,失败时返回-1。
注意,用 root
创建的共享内存,不管创建的权限是什么,普通用户无法删除。
[外链图片转存中…(img-v6qW3XRA-1720711279572)]
[外链图片转存中…(img-CG0tGAne-1720711279572)]