十三、Linux编程——信号

十三、Linux编程——信号


一、信号的基本认识

目前,信号仍然是Linux操作系统的重要通信手段

1.信号的特质

信号是软件层面上的 “中断”(软中断),一旦信号产生,无论程序执行到什么位置,都必须立即停止运行,处理信号,当信号处理结束时,才继续执行后续指令(每个进程收到的所有信号,都是由内核发送的,并由内核处理信号;因此我们通过所有手段制造的信号,都只是驱使内核产生信号,而手段本身并不产生信号)

信号的共性:(1)简单、(2)不能携带大量信息、(3)满足条件才发送

2.与信号相关的事件

产生信号的原因

  • 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
  • 系统调用产生,如:kill、raise、abort函数
  • 软件条件产生,如:定时器alarm、setitimer函数
  • 硬件异常产生,如:非法访问内存(段错误)、除以0(浮点数例外)、内存对齐出错(总线错误)
  • 命令产生,如:kill命令

未决:产生和递达之间的状态,主要由于阻塞(屏蔽)导致该状态

递达:递送并且到达进程,直接被内核处理

信号的处理方式

  • 执行默认动作
  • 忽略
  • 捕捉(调用用户处理函数,自定义)
3.信号集合和状态(重要)

阻塞信号集(信号屏蔽字): 阻塞信号的集合,其本质是 位图,用于记录信号的屏蔽状态。一旦信号被屏蔽,那么该信号在解除屏蔽前,将一直处于未决状态

未决信号集:未决信号的集合,其本质是 位图,用于记录信号处理状态。该集合里的信号,表示已产生,但尚未被处理的信号

  • 当信号产生,未决信号集 中描述该信号的位立刻翻转为 1,表示该信号处于未决状态;而当信号被处理后,对应的位翻转回为 0,这一时刻往往十分短暂

  • 当信号产生后由于某些原因(主要是阻塞)不能抵达,则这类信号的集合称之为未决信号集;在屏蔽被解除前,信号一直处于未决状态

在这里插入图片描述

4.信号的编号

可以使用 kill –l 终端命令查看当前系统可使用的信号及编号有哪些

其中 1 ~ 31 属于常规信号,这些信号都有其默认事件和处理动作;34 ~ 64 属于实时信号,没有默认事件

root@ubuntu:~# kill -l
 1) SIGHUP		 2) SIGINT		 3) SIGQUIT	 	 4) SIGILL		 5) SIGTRAP
 6) SIGABRT		 7) SIGBUS		 8) SIGFPE		 9) SIGKILL		10) SIGUSR1
11) SIGSEGV		12) SIGUSR2		13) SIGPIPE		14) SIGALRM		15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD		18) SIGCONT		19) SIGSTOP		20) SIGTSTP
21) SIGTTIN		22) SIGTTOU		23) SIGURG		24) SIGXCPU		25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF		28) SIGWINCH	29) SIGIO		30) SIGPWR
31) SIGSYS		34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX
5.信号的四要素

包括:(1)名称、(2)编号、(3)事件、(4)默认处理动作;在使用信号之前,应先确定其四要素,然后再用!

可通过 man 7 signal 终端命令查看帮助文档获取,也可以查看 /usr/include/x86_64-linux-gnu/bits/signum.h 头文件

其中,操作系统中有一部分信号在不同架构的芯片下,其信号的编号可能有所不同;如下图中有三列编号的信号,其中中间一列信号编号是操作系统运行在 x86、arm和其他大部分架构 的芯片中的信号的编号;因此在使用信号时,直接使用可避免出错
在这里插入图片描述
默认动作包括

  • Term:终止进程
  • Ign: 忽略信号(默认即时对该种信号忽略操作)
  • Core:终止进程,生成 Core 文件(查验进程死亡原因, 用于 gdb 调试)
  • Stop:停止/暂停进程
  • Cont:继续运行进程
6.Linux常规信号信息一览表

注意

  • 9) SIGKILL19) SIGSTOP 两信号,不能被忽略、处理和阻塞,只能执行默认动作
  • 10) SIGUSE112) SIGUSR2 两信号,没有默认事件
  • 17) SIGCHLD 默认处理动作为忽略该信号

重点掌握:1 ~ 3、7 ~ 9、10 ~ 15、17、19

编号 名称									事件														默认处理动作
--------------------------------------------------------------------------------------------------------------------------------------------------------
 1) SIGHUP:		当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号								终止进程
 2) SIGINT: 	当用户按下 Ctrl+c 时,用户终端向正在运行中的由该终端启动的程序发出此信号						终止进程
 3) SIGQUIT: 	当用户按下 ctrl+\ 时产生,用户终端向正在运行中的由该终端启动的程序发出些信号					终止进程
 4) SIGILL: 	CPU 检测到某进程执行了非法指令															终止进程并产生 core 文件
 5) SIGTRAP:	该信号由断点指令或其他 trap 指令产生														终止里程 并产生 core 文件
 6) SIGABRT:	调用 abort 函数时产生该信号																终止进程并产生 core 文件
 7) SIGBUS:		非法访问内存地址,包括内存对齐出错															终止进程并产生 core 文件
 8) SIGFPE:		在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误			终止进程并产生 core 文件
 9) SIGKILL: 	无条件终止进程,本信号不能被忽略、处理和阻塞												终止进程,它向系统管理员提供了可以杀死任何进程的方法
10) SIGUSE1:	用户定义的信号,即程序员可以在程序中定义并使用该信号											终止进程
11) SIGSEGV:	指示进程进行了无效内存访问																终止进程并产生 core 文件
12) SIGUSR2:	另外一个用户自定义信号,程序员可以在程序中定义并使用该信号  									终止进程
13) SIGPIPE:	Broken pipe 向一个没有读端的管道写数据													终止进程
14) SIGALRM: 	定时器超时,超时的时间由系统调用 alarm 设置												终止进程
15) SIGTERM: 	程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止,通常用来要示程序正常退出,			终止进程
				执行 shell 命令 Kill 时,缺省产生这个信号
				
16) SIGSTKFLT:	Linux 早期版本出现的信号,现仍保留向后兼容													终止进程
17) SIGCHLD: 	子进程状态发生变化时,父进程会收到这个信号													忽略这个信号
18) SIGCONT: 	如果进程已停止,则使其继续运行																继续/忽略
19) SIGSTOP: 	停止进程的执行,信号不能被忽略、处理和阻塞													暂停进程
20) SIGTSTP: 	停止终端交互进程的运行。按下 ctrl+z 时发出这个信号											暂停进程
21) SIGTTIN: 	后台进程读终端控制台																		暂停进程
22) SIGTTOU: 	该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生										暂停进程
23) SIGURG: 	套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达,如:网络带外数据到达	忽略该信号
24) SIGXCPU: 	进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程						终止进程
25) SIGXFSZ: 	超过文件的最大长度设置																	终止进程
26) SIGVTALRM: 	虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间			终止进程
27) SGIPROF: 	类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间						终止进程
28) SIGWINCH: 	窗口变化大小时发出																		忽略该信号
29) SIGIO: 		此信号向进程指示发出了一个异步 IO 事														忽略
30) SIGPWR: 	关机																					终止进程
31) SIGSYS: 	无效的系统调用																			终止进程并产生 core 文件

(注:该表摘自黑马程序员课程)


二、信号的产生和定时器
1.kill命令产生信号

命令产生的信号

kill -[SIGKILL] [pid]

向指定 pid 进程发送信号

-[SIGKILL]
其中 SIGKILL 表示需要发送的信号的编号

[pid]
信号递达的进程的进程号 pid

2.kill()函数产生信号

包含头文件

#include <unistd.h>
#include <signal.h>

int kill(pid_t pid, int sig);

向指定进程发送信号

pid_t pid 进程号 pid

pid_t pid含义
pid > 0发送信号给指定进程
pid = 0发送信号给跟调用 kill 函数的那个进程处于同一进程组的进程
pid < -1取绝对值,发送信号给该绝对值所对应的进程组的所有组员
pid = -1发送信号给有权限发送的所有进程(较为危险)

int sig 要发送的信 号的编号,建议使用宏
返回值 成功时返回 0,失败返回 -1errno

进程组:当父进程创建子进程时,子进程会和父进程同属于一个进程组,且该组的组长为父进程,进程组ID为父进程的PID

例如:子进程调用 kill 向父进程发送 SIGKILL 信号,终结父进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    pid_t pid = fork();
    sys_err(pid, "fork error");
    if(pid == 0)
    {
        sleep(2);
        int ret = kill(getppid(), SIGKILL);
        sys_err(ret, "kill SIGKILL error");
    }else
    {
        int i = 0;
        while(1)
        {
            printf("i = %d\n", i++);
            sleep(1);
        }
    }

    return 0;
}

在这里插入图片描述
💥(了解)其他几个发送信号的函数:int raise(int sig);void abort(void);

3.alarm()函数——定时器

包含头文件

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

设置定时器(闹钟),当经过指定的秒数 seconds 时,内核会给当前进程发送 14) SIGALRM 信号,进程收到该信号,默认动作终止;
每个进程都有且只有唯一个定时器;
在上一个闹钟时间到来之前,再次调用 alarm,可以重置上一个闹钟,并返回上一个闹钟的剩余时间

unsigned int seconds 定时器(闹钟)的秒数
返回值 (unsigned int) 返回上一个闹钟所剩下的时间,该函数不会出错;若返回 0,则表示没有使用 alarm 进行定时


unsigned int alarm(0);

取消定时器(闹钟)(利用了闹钟重置机制,并定时 0 秒——不定时)

返回值 (unsigned int) 返回旧闹钟余下的秒数

自然计时法:用 alarm 定时就是该法,即计时与进程状态无关!就绪、运行、挂起(阻塞、sleep()、暂停)、终止、僵尸…无论进程处于何种状态,alarm 都会计时

运行文件时time ./filename,可查看程序的执行时间,实际执行实际(real) = 用户时间(user) + 系统时间(sys) + 等待时间
在这里插入图片描述
程序运行的瓶颈在于I/O,优化程序速度,首选优化I/O

4.setitimer——定时器

包含头文件

#include <sys/time.h>

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

设置定时器(闹钟),可以代替 alarm() 函数,精度微妙 us,可实现周期定时

int which 指定计时的形式

int which含义发送信号
ITIMER_REAL和 alarm() 一样,使用的是自然计时法,该方式使用得最多14) SIGALRM
ITIMER_VIRTUAL(了解)虚拟空间计时法(用户空间),即只计算进程占用CPU的时间26) SIGVTALRM
ITIMER_PROF(了解)运行时计时(用户+内核)27) SIGPROF

const struct itimerval *new_value 设置定时器(闹钟)的 首次触发时间以后触发周期
其结构体如下

struct itimerval {
	struct timeval it_interval; /* Interval for periodic timer */
	struct timeval it_value;    /* Time until next expiration */
};

struct timeval {
	time_t      tv_sec;         /* seconds 秒 */
	suseconds_t tv_usec;        /* microseconds 微秒 */
};

它相当于

struct itimerval {
	struct timeval { 				/* Interval for periodic timer */
		time_t      tv_sec;         /* seconds 秒 */
		suseconds_t tv_usec;        /* microseconds 微秒 */
	}it_interval;

	struct timeval { 				/* Time until next expiration */
		time_t      tv_sec;         /* seconds 秒 */
		suseconds_t tv_usec;        /* microseconds 微秒 */
	}it_value;
};
struct itimerval含义
it_interval首次触发后,定时器触发的周期
it_value定时器(闹钟)的首次触发时间

(其中 time_t tv_secsuseconds_t tv_usec 分别设置 微秒,若两个参数都设置为 0,则表示 清零 定时器)

struct itimerval *old_value 传出参数,表示上一次定时剩余的时间,其类型和 new_value 一样
返回值 成功时返回 0,失败返回 -1errno

例如:设置首次触发时间为 2.0s,以后触发周期为 5.0s,观察 setitimer() 函数现象

这里先提前使用 signal() 函数,其中 signal(SIGALRM, myfunc); 表示捕捉 SIGALRM 信号,捕捉到后执行 myfunc() 函数

这个函数下面会讲

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>

void myfunc(int signo)
{
    printf("Hello World!\n");
}

int main(int argc, char *argv[])
{
    struct itimerval new_time = {{5, 0}, {2, 0}}; // 定时时间:5.0s,周期: 2.0s
    struct itimerval old_time; // 传出参数,获取定时器剩余时间

    signal(SIGALRM, myfunc); // 注册 SIGALRM 信号的捕捉处理函数

    int ret = setitimer(ITIMER_REAL, &new_time, &old_time); // 设置定时器,并传入 new_time old_time
    if(ret == -1) // 出错判断
    {
        perror("setitimer error");
        exit(1);
    }

    int seconds = 1;
    while(1) // 每隔 1s 打印一次时间,用来观察现象
    {
        printf("seconds = %d\n", seconds++);
        sleep(1);
    }

    return 0;
}

发现首次触发时间为 2s,而周期触发时间为 5s
在这里插入图片描述


三、信号编程

🔺用户可以通过调用函数来操作信号屏蔽字,但不能操作未决信号集,但可以通过操作信号屏蔽字的方式来影响未决信号集

其中 sigset_t set 为供用户使用的集合,该集合可以和信号屏蔽字 mask 进行位运算,进而操作信号屏蔽字
在这里插入图片描述

1.信号集操作函数

操作用户自定义集合 sigset_t *set

包含头文件

#include <signal.h>

int sigemptyset(sigset_t *set);

清空 sigset_t *set 集合,即将用户自定义集合置 0


int sigfillset(sigset_t *set);

sigset_t *set 用户自定义集合置 1

sigset_t *set 即用户自定义集合
返回值 成功时返回 0,失败返回 -1errno

int sigaddset(sigset_t *set, int signum);

sigset_t *set 用户自定义集合中指定编号的信号位置 1


int sigdelset(sigset_t *set, int signum);

sigset_t *set 用户自定义集合中指定编号的信号位置 0


sigset_t *set 即用户自定义集合
int signum 指定信号的编号
返回值 成功时返回 0,失败返回 -1errno

int sigismember(const sigset_t *set, int signum);

查看 const sigset_t *set 中指定编号的信号是否置 1,即查看信号是否被阻塞

const sigset_t *set 即用户自定义集合
int signum 指定信号的编号
返回值 是则返回 1,否则返回 0,失败返回 -1errno

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

使用 sigset_t *set 来对信号屏蔽字 mask 进行位操作;
即用来屏蔽信号、解除信号屏蔽

int how 要进行的操作

int how含义相当于
SIG_BLOCK设置为此值时,sigset_t *set 中的 1 表示屏蔽信号mask |= set
SIG_UNBLOCK设置为此值时,sigset_t *set 中的 1 表示解除屏蔽mask &= ~set
SIG_SETMASK直接用 sigset_t *set 覆盖信号屏蔽字 mask,不建议使用mask = set

const sigset_t *set 即用户自定义集合
sigset_t *oldset 传出参数,返回原来的信号屏蔽字集合,即用于保存原状态,便于恢复
返回值 成功时返回 0,失败返回 -1errno

✅查看未决信号集 pending

int sigpending(sigset_t *set);

查看未决信号集 pending

sigset_t *set 传出参数,即返回未决信号集
返回值 成功时返回 0,失败返回 -1errno

例如:将信号 2) SIGINT 屏蔽,并在终端按下 Ctrl + c,观察未决信号集中 2) SIGINT 信号的对应位是否置 1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

void print_set(sigset_t *set) // 定义函数,用于打印信号集
{
    int i;
    for(i=1; i<32; i++)
    {
        printf("%d", sigismember(set, i));
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    sigset_t set, oldset, pedset; // 定义三个 sigset_t 类型,分别代表 用户自定集、原信号屏蔽字、未决信号集

    int ret = sigemptyset(&set); // 将 set 置 0
    sys_err(ret, "sigemptyset set error");

    ret = sigaddset(&set, SIGINT); // 将 set 的 SIGINT 信号位设置为1
    sys_err(ret, "sigaddset error");

    ret = sigprocmask(SIG_BLOCK, &set, &oldset); // 将 set 中信号位为 1 的信号屏蔽,并接收原信号屏蔽字
    sys_err(ret, "sigprocmask error");

    while(1)
    {
        ret = sigpending(&pedset); // 获取未决信号集
        sys_err(ret, "sigpending error");

        print_set(&pedset);
        sleep(1);
    }

    return 0;
}

发现当按下 Ctrl + c 时,由于 2) SIGINT 信号被屏蔽,未决信号集中的第二个位置了 1,表示 2) SIGINT 信号为未决信号
在这里插入图片描述

2.信号捕捉(重点)

包含头文件

#include <signal.h>

sighandler_t signal(int signum, sighandler_t handler);

注册一个信号捕捉函数;
即当编号为 signum 的信号时递达时,捕捉它,并执行函数 handler

int signum 指定信号的编号
sighandler_t handler 要注册的函数,不能随意定义,须按照 typedef 来定义
原型如下:定义一个 返回值为空,参数为一个整型 的函数指针类型

typedef void (* sighandler t)(int); // 定义一个(返回值为空,参数为一个整型)的函数指针类型

由于历史原因,在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行为;因此应该尽量避免使用它,取而代之使用 sigaction 函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

修改信号处理动作(通常)

int signum 要捕捉的信号编号
const struct sigaction *act 参数结构体
其结构体如下

struct sigaction {
	void     (*sa_handler)(int); 						// 捕捉到信号后要执行的函数(返回值为空,参数为一个整型)
	void     (*sa_sigaction)(int, siginfo_t *, void *); // 另一个格式的函数,几乎不使用
	sigset_t   sa_mask; 								// 只在信号捕捉函数执行期间生效的信号屏蔽字,来防止信号入侵
	int        sa_flags;								// 指定在信号捕捉函数执行期间捕捉到某些信号时的行为
	void     (*sa_restorer)(void); 						// Linux中已废弃
};

sa_mask 大部分情况下进行清空处置:sigemptyset(&(act.sa_mask));
int sa_flags 大部分情况下设置为 0,表示使用默认属性,则在信号捕捉函数执行期间,原捕捉信号默认被屏蔽

struct sigaction *oldact 传出参数,返回默认处理动作,即用来保存原动作状态,便于恢复,不使用时可传 NULL
返回值 成功时返回 0,失败返回 -1errno

例如:捕捉 Ctrl + c 发出的 2) SIGINT 信号和 Ctrl + \ 发出的 3) SIGQUIT 信号

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

void myfunc(int signo) 			// 信号捕捉函数
{
    if(signo == SIGINT) 		// 如果捕捉到的是 SIGINT 信号
    {
        printf("\nSIGINT signal was successfully captured!\n");
    }else if(signo == SIGQUIT) 	// 如果捕捉到的是 SIGQUIT 信号
    {
        printf("\nSIGQUIT signal was successfully captured!\n");
    }
}

int main(int argc, char *argv[])
{
    struct sigaction act, oldact1, oldact2;

    /* 初始化 act */
    act.sa_handler = myfunc;        // set callback function name
    sigemptyset(&(act.sa_mask));    // set mask when myfunc working
    act.sa_flags = 0;               // set usually use
    /* 初始化结束 */

    int ret = sigaction(SIGINT, &act, &oldact1); // 捕捉 SIGINT 信号
    sys_err(ret, "sigaction SIGINT error");

    ret = sigaction(SIGQUIT, &act, &oldact2);	// 捕捉 SIGQUIT 信号
    sys_err(ret, "sigaction SIGQUIT error");

    while(1);

    return 0;
}

在这里插入图片描述
信号捕捉特性

  • 捕捉函数执行期间,信号屏蔽字 由 mask → sa_mask , 当捕捉函数执行结束时,恢复回 mask
  • 捕捉函数执行期间,原捕捉信号将自动被屏蔽(sa_flgs = 0 时)
  • 捕捉函数执行期间,若被屏蔽信号被一次或多次发送,捕捉函数执行结束后内核只处理 1 次!(即被屏蔽信号不支持排队
3.内核实现信号捕捉原理(了解)

在这里插入图片描述
(注:该图摘自黑马程序员课程)


四、借助信号捕捉回收子进程
1.SIGCHLD信号

17) SIGCHLD 的产生条件

  • 子进程终止时
  • 子进程接收到 19) SIGSTOP
  • 子进程处于停止态,在接收到 18) SIGCONT 后唤醒时
2.借助SIGCHLD信号回收子进程

在调用 exec 族函数时,由于 exec 族函数调用成功后会“逃离现场”,因此无法使用 wait 函数来回收子进程;但通过发信号可以

例如:利用 for 循环创建 5 个子进程,并借助 17) SIGCHLD 信号回收子进程

🔺其中下列代码的目的是:

在一次信号捕捉函数执行期间,期间可能会出现多个子进程一起结束,相继发出 17) SIGCHLD 信号的情况,但由于 被屏蔽的信号不支持排队,内核只会处理 1 次这些信号,因此如果不加以干预,可能有些子进程不能被回收

while((pid = waitpid(-1, NULL, WNOHANG)) != -1) // 非阻塞式回收
{
    if(pid > 0) // 若回收了子进程,则打印进程 pid
    {
        printf("Reclaim pid = %d child process sucessfully!\n", pid);
    }else if(pid == 0)
    	return;
}

上述代码中,当有多个子进程可以被回收时,waitpid 会把这些可以回收的子进程一并回收掉

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>

void sys_err(int ret, const char *str)
{
    if(ret == -1)
    {
        perror(str);
        exit(1);
    }
}

void reclaim_child(int signo) // 信号捕捉函数:回收子进程
{
    pid_t pid;

    while((pid = waitpid(-1, NULL, WNOHANG)) != -1) // 非阻塞式回收
    {
        if(pid > 0) // 若回收了子进程,则打印进程 pid
        {
            printf("Reclaim pid = %d child process sucessfully!\n", pid);
        }
    }
}

int main(int argc, char *argv[])
{
    pid_t pid;
    int i;

    for(i=0; i<5; i++) // 循环创建 5 个子进程
    {
        pid = fork();
        sys_err(pid, "fork error");
        if(pid == 0)
        {
            break;
        }
    }

    if(i == 5) // 在父进程中注册捕捉信号函数
    {
        struct sigaction act;

        /* act initialization */
        act.sa_handler = reclaim_child;
        sigemptyset(&(act.sa_mask));
        act.sa_flags = 0;
        /* end */

        int ret = sigaction(SIGCHLD, &act, NULL); // 注册捕捉 SIGCHLD 信号函数
        sys_err(ret, "sigaction error");

        sleep(1); // 防止父进程先于子进程结束,导致无法回收
    }else
    {
        printf("I'm child, pid = %d\n", getpid());
    }

    return 0;
}

在这里插入图片描述
注意:如果在父进程注册捕捉信号函数之前,所有子进程都已结束了,则父进程就无法捕捉 17) SIGCHLD 信号,也就无法回收子进程了;如果在父进程注册捕捉信号函数之前,至少有一个子进程没结束,则都可以通过捕捉 17) SIGCHLD 信号来结束所有子进程

✅为了解决这个可能产生的问题,我们可以在 fork() 之前将 17) SIGCHLD 信号屏蔽掉,使该信号处于未决状态,在父进程注册捕捉信号函数之后再解除对 17) SIGCHLD 信号的屏蔽,即可防止子进程提前全体结束

部分代码参考

// main() 函数:
// 屏蔽 SIGCHLD 信号
sigset_t set;
sigemptyset(&set);
sigaddset(SIGCHLD, &set);
sigprocmask(SIG_BLOCK, &set, NULL);

/* fork()               */
/* 父进程注册信号捕捉函数 */
/* 代码在此              */

// 解除 SIGCHLD 屏蔽
sigprocmask(SIG_UNBLOCK, &set, NULL);

五、中断系统调用

系统调用可分为三类:快速系统调用、慢速系统调用、其他系统调用

  • 快速系统调用: 一些系统调用立即返回, “立即”意味着它只需极短的处理器时间,他们的持续时间(除了实时系统)没有硬性限制,但是这些调用一经预定足够长时间就会返回,这些系统调用通常称为 非阻塞
  • 慢速系统调用:这类系统调用可能不会立即返回,是可能会使进程永久阻塞的一类,例如:这类系统调用在阻塞执行期间,突然收到一个信号,该系统调用就会被中断,先由内核处理信号,但当内核处理完信号后,却不回到系统调用的位置继续执行(早期),如:read、write、pause、wait…这类系统调用

✅在通过 sigaction() 函数捕捉信号,我们可以设置 sa_flags = SA_RESTART,使得捕捉信号并处理完信号后,能够重新启动系统调用,默认情况下不重启系统调用

扩展了解
sa_flags 还有很多可选参数,能适用于不同情况,例如:捕捉到信号后,在执行捕捉函数期间,不希望自动屏蔽/阻塞该信号,可将 sa_flags 设置为 SA_NODEFER,除非 sa_mask 中包含该信号。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值