高并发reactor服务器[中]

avatar
作者
筋斗云
阅读量:4

高并发reactor服务器[中]

四、进程控制和进程同步

1.信号

1.1 信号的基本概念

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

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

kill -信号的类型 进程编号 killall -信号的类型 进程名 

1.2 信号的类型

信号名信号值默认处理动作发出信号的原因
SIGHUP1A终端挂起或者控制进程终止
SIGINT2A键盘中断 Ctrl+c
SIGQUIT3C键盘的退出键被按下
SIGILL4C非法指令
SIGTRAP5C断点指令
SIGABRT6C由abort(3)发出的中止信号
SIGBUS7C总线错误
SIGFPE8C浮点异常
SIGKILL9Akill -9 杀死进程,该信号不能被捕获和忽略
SIGUSR110A用户定义信号1
SIGSEGV11C无效的内存引用(数组越界、操作空指针)
SIGUSR212A用户定义信号2
SIGPIPE13A向一个无读进程的管道写数据
SIGALRM14A闹钟信号,由alarm()函数发出的信号
SIGTERM15A终止信号,默认发送的信号
SIGSTKFLT16A栈错误
SIGCHLD17B子进程结束时发出
SIGCONT18D继续执行已经停止的进程
SIGSTOP19D停止进程
SIGTSTP20D终端按下停止键
SIGTTIN21D后台进程请求读终端
SIGTTOU22D后台进程请求写终端
SIGURG23B紧急条件检测(套接字)
SIGXCPU24C超出CPU时间限制
SIGXFSZ25C超出文件大小限制
SIGVTALRM26A虚拟时钟信号
SIGPROF27A分析时钟信号
SIGWINCH28B窗口大小变化
SIGPOLL29B轮询(Sys V)
SIGPWR30A电源故障
SIGSYS31C非法系统调用

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); 
  • shmaddrshmat() 函数返回的地址。

调用成功时返回0,失败时返回-1。

7.4 shmctl函数

该函数用于操作共享内存,最常用的操作是删除共享内存。

int shmctl(int shmid, int command, struct shmid_ds *buf); 
  • shmidshmget() 函数返回的共享内存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); 
  • shmidshmget() 函数返回的共享内存id。
  • command 操作共享内存的指令,如果要删除共享内存,填 IPC_RMID
  • buf 操作共享内存的数据结构的地址,如果要删除共享内存,填0。

调用成功时返回0,失败时返回-1。

注意,用 root 创建的共享内存,不管创建的权限是什么,普通用户无法删除。

[外链图片转存中…(img-v6qW3XRA-1720711279572)]

[外链图片转存中…(img-CG0tGAne-1720711279572)]外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.5 循环队列

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

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!