高并发reactor服务器[中]

四、进程控制和进程同步

1.信号

1.1 信号的基本概念

信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但不能给进程传递任何数据。

信号产生的原因有很多,在Shell中,可以用killkillall命令发送信号:

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 信号的处理

进程对信号的处理方法有三种:

  1. 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
  2. 设置中断的处理函数,收到信号后,由该函数来处理。
  3. 忽略某个信号,对该信号不做任何处理,就像未发生过一样。

signal()函数可以设置程序对信号的处理方式。

函数声明:

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数说明:

  • sig:指定要捕获的信号。
  • func:指向信号处理函数的指针。处理函数需要接收一个整型参数,这个参数是捕获的信号编号。
  1. SIG_DFL:SIG_DFL宏表示默认的信号处理方式。使用SIG_DFL作为signal函数的第二个参数时,表示对该信号采用系统默认的处理方式。
  2. SIG_IGN:SIG_IGN宏表示忽略信号。使用SIG_IGN作为signal函数的第二个参数时,表示进程在接收到该信号时将忽略它,不进行任何处理。这在某些情况下可以防止进程被意外终止或中断。
  3. SIG_ERR:SIG_ERR宏用于指示错误。它并不是作为signal函数的第二个参数来使用,而是作为signal函数的返回值来表示调用失败。如果signal函数的调用失败,它将返回SIG_ERR。这通常用于检测和处理signal函数调用中的错误。

image-20240709113614147

image-20240709113230874

image-20240709113240944

1.4 信号有什么用

服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为进程被杀的时候,是突然死亡,没有安排善后工作。

如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。

向服务程序发送 0 的信号,可以检测程序是否存活。

image-20240709135336848

1.5 发送信号

Linux操作系统提供了 killkillall 命令向程序发送信号,在程序中,可以用 kill() 库函数向其它进程发送信号。

函数声明:

int kill(pid_t pid, int sig);

kill() 函数将参数 sig 指定的信号传给参数 pid 指定的进程。

参数 pid 有几种情况:

  1. pid > 0 将信号传给进程为 pid 的进程。
  2. pid = 0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意这个行为依赖于系统实现。
  3. pid < -1 将信号传给进程组ID为 |pid| 的所有进程。
  4. pid = -1 将信号传给所有有权限发送信号的进程,但不包括发送信号的进程。

2.进程终止

有8种方式可以中止进程,其中5种为正常终止,它们是:

  1. main() 函数用 return 返回;
  2. 在任意函数中调用 exit() 函数;
  3. 在任意函数中调用 _exit()_Exit() 函数;
  4. 最后一个线程从其启动例程(线程主函数)用 return 返回;
  5. 在最后一个线程中调用 pthread_exit() 返回;

异常终止有3种方式,它们是:

  1. 调用 abort() 函数中止;
  2. 接收到一个信号;
  3. 最后一个线程对取消请求做出响应。

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 进程终止的状态。

image-20240709143530327

image-20240709143615950

2.2 资源释放问题

  • return 表示函数返回,会调用局部对象的析构函数,main() 函数中的 return 还会调用全局对象的析构函数。
  • exit() 表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。
  • _exit()_Exit() 直接退出,不会执行清理工作。

2.3 进程的终止函数

进程可以用 atexit() 函数登记终止函数(最多32个),这些函数将由 exit() 自动调用。

int atexit(void (*function)(void));

exit() 调用终止函数的顺序与登记时相反。

image-20240709143824286

image-20240709143830549

3.调用可执行程序

3.1 system() 函数

system()函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()函数就行了。

函数声明:

int system(const char * string);

system()函数的返回值比较麻烦。

  1. 如果执行的程序不存在,system()函数返回非0;
  2. 如果执行程序成功,并且被执行的程序终止状态是0,system()函数返回0;
  3. 如果执行程序成功,并且被执行的程序终止状态不是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. 如果执行程序失败则直接返回-1,失败原因存于errno中。
  2. 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
  3. 如果执行成功则函数不会返回,当在主程序中成功调用exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行。
  4. 在实际开发中,最常用的是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()之后,父进程和子进程的执行顺序是不确定的。

image-20240709221535371

image-20240709221546617

4.4 fork()的两种用法

  1. 父进程希望复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),让子进程处理这些请求,而父进程则继续等待下一个连接请求。
  2. 进程要执行另一个程序。这种用法在Shell中很常见,子进程从fork()返回后立即调用exec

4.5 共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。

如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。

image-20240709222929369

image-20240709222803641

此时可以看见只有十万行数据。

image-20240709222853769

image-20240709223236254

此时应该是二十万行数据,少一行可能是文件写入操作并不是原子的,在没有同步机制的情况下,两个进程可能在同一时间尝试写入文件的不同部分,导致写入的数据相互干扰。

4.6 vfork()函数

vfork()函数的调用和返回值与fork()相同,但两者的语义不同。

vfork()函数用于创建一个新进程,而该新进程的目的是exec一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。

vfork()fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用execexit之后父进程才恢复运行。

5.僵尸进程

在操作系统中,僵尸进程(Zombie Process)是指已经终止但其父进程尚未读取其退出状态的子进程。僵尸进程虽然不再运行,但仍然占据进程表中的一个条目,以便内核能保存该进程的退出状态信息(如进程ID、退出状态等),直到父进程读取这些信息。

5.1 造成僵尸进程的原因

如果父进程比子进程先退出,子进程将由1号进程托管(这也是一种让进程在后台运行的方法)。

如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程。

5.2 僵尸进程的危害

内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构。父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因没有可用的进程编号而导致系统不能产生新的进程。

5.3 避免僵尸进程的方法

  1. 处理SIGCHLD信号:当子进程退出时,内核会向父进程发送SIGCHLD信号。如果父进程用signal(SIGCHLD, SIG_IGN)通知内核表示自己对子进程的退出不感兴趣,那么子进程退出后会立刻释放其数据结构。
  2. 使用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) 可获取终止进程的信号。

image-20240709230911352

image-20240709231034423

image-20240709231050581

image-20240709231124375

image-20240709231140813

如果父进程很忙,可以捕获 SIGCHLD 信号,在信号处理函数中调用 wait()/waitpid()

image-20240709231439475

image-20240709231422927

6.多进程与信号

[进程间发送信号](##1.5 发送信号)

在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出。

如果父进程收到退出信号,应该向全部子进程发出退出信号,然后自己退出。

image-20240711222919564

image-20240711222900141

image-20240711223111481

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(系统内存不足,没有权限)。

image-20240711224223200

image-20240711224212293

ipcs -m 可以查看系统的共享内存,包括:键值(key),共享内存id(shmid),拥有者(owner),权限(perms),大小(bytes)。

ipcrm -m 共享内存id 可以手动删除共享内存,如下:

image-20240711225202860

注意:共享内存中的数据类型不能使用容器,只能用基本数据类型。

7.2 shmat函数

该函数用于把共享内存连接到当前进程的地址空间。

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmidshmget() 函数返回的共享内存标识。
  • 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 创建的共享内存,不管创建的权限是什么,普通用户无法删除。

image-20240711230653886

image-20240711230522921

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)]外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.5 循环队列

7.6 基于共享内存的循环队列

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-12 09:24:03       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-12 09:24:03       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-12 09:24:03       57 阅读
  4. Python语言-面向对象

    2024-07-12 09:24:03       68 阅读

热门阅读

  1. GESP C++ 三级真题(2024年3月)T2 完全平方数

    2024-07-12 09:24:03       22 阅读
  2. 【排序 - 选择排序优化版(利用堆排序)】

    2024-07-12 09:24:03       22 阅读
  3. 【贪心算法题记录】134. 加油站

    2024-07-12 09:24:03       24 阅读
  4. 超级源点/汇点(算法篇)

    2024-07-12 09:24:03       30 阅读
  5. 【MySQL】6.表的增删查改(CURD)

    2024-07-12 09:24:03       23 阅读
  6. 开源项目的机遇与挑战

    2024-07-12 09:24:03       24 阅读
  7. 从0到1搭建数据中台(2):数据中台架构

    2024-07-12 09:24:03       24 阅读
  8. 【C/C++】内存相关

    2024-07-12 09:24:03       25 阅读
  9. 【LeetCode 0169】【摩尔投票算法】主元素

    2024-07-12 09:24:03       24 阅读
  10. 每日一道算法题 LCR 151. 彩灯装饰记录 III

    2024-07-12 09:24:03       30 阅读
  11. 【随想】社交

    2024-07-12 09:24:03       22 阅读
  12. 谷歌独立站:纯净网络空间,自由与创新的融合

    2024-07-12 09:24:03       25 阅读
  13. Centos解决服务器时间不准的问题

    2024-07-12 09:24:03       23 阅读