Linux学习笔记系列
前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
文章目录
一、前言
本节主要介绍下信号的相关概念和用法,其实这玩意不能算在进程间通信的范畴,但是后面会有涉及就把它也给拎进来介绍下。
二、前置条件
UB18 + 一点点的基础知识
三、本文参考资料
Unix卷2
百度
野火Linux教程
四、正文部分
4.1 信号概述
信号(signal),又称为软中断信号,用于通知进程发生了异步事件,
它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,
在原理上, 一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
正如我们所了解的中断服务函数一样, 在中断发生的时候,就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候, 也会相应地采取一些行动。
我们可以使用术语“生成(raise)”表示一个信号的产生, 使用术语“捕获(catch)”表示进程接收到一个信号。
在Linux系统中,信号可能是由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。
由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由shell和终端处理器生成并且引起中断。
由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应并且去处理它。
无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。
进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
4.2 系统支持的信号
使用kill -l 命令查看
从图中可以看出,Linux系统支持信号62种信号,每种信号名称都以SIG三个字符开头,
注意,编号为32和33的信号值是不存在的。
注意:
信号的“值”在 x86、PowerPC 和 ARM平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
“描述”中注明的一些情况发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用kill()函数来产生任何信号。
信号 SIGKILL 和 SIGSTOP 是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
一般而言,信号的响应处理过程如下:
如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。
如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;
如果设置为忽略,那么直接丢弃该信号。
最后才执行信号的默认处理。
可以将这62种信号分为2大类:
信号值为1~31的信号属性非实时信号(也称为不可靠信号),它们是从UNIX系统中继承下来的信号,具体的作用见下表(略),
信号值为34~64的信号为实时信号(也被称为可靠信号)。
4.3 非实时信号与实时信号
Linux 系统中有许多信号,其中前面 31 个信号都有一个特殊的名字,对应一个特殊的事件,
比如 信号值为1的信号SIGHUP(Signal Hang UP),这个信号就是通知系统关闭中断的,当系统中的一个控制终端被关闭(即挂断,hang up)时,都会产生这个信号。
信号值为1~31的信号属性非实时信号,它主要是因为这类信号不支持排队, 因此信号可能会丢失。
比如发送多次相同的信号,进程只能收到一次,也只会处理一次,因此剩下的信号将被丢弃。
而实时信号(信号值为34~64的信号)则不同,它是支持排队的,发送了多少个信号给进程,进程就会处理多少次。
- 值小的信号优先级大于值大的信号
- 非实时信号提交给它的信号处理程序时的唯一参数是该信号的值,而实时信号携带更多信息,
typedef struct {
int si_signo;
/* SI_USER/QUEUE/TIMER/ASYNCIO/MEGEQ */
int si_code;
union sigval si_value;
} siginfo_t
为什么说信号还有可靠与不可靠呢,这得从信号的处理过程来介绍了:
一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!
一般是在中断返回的时候,或者内核态返回用户态的时候(这种情况是比较常见的处理方式)。
也就是说,即使这些信号到来了,进程也不一定会立即去处理它,
因为系统不会为了处理一个信号而把当前正在运行的进程挂起,这样的话系统的资源消耗太大了,如果不是紧急信号,是不会立即处理的,
所以系统一般都会选择在内核态切换回用户态的时候处理信号。
比如有时候进程处于休眠状态,但是又收到了一个信号,于是系统就得把信号储存在进程唯一的PCB(进程控制块)当中。
而非实时信号是不支持排队的,假如此时又有一个信号到来,那么它将被丢弃,这样进程就无法处理这个信号,所以它是不可靠的。
对于实时信号则没有这种顾虑,因为它支持排队,信号是不会被丢弃的, 这样子每个到来的信号都能得到有效处理。
4.4 信号生成
生成信号的事件一般可以归为3大类:程序错误、外部事件以及显式请求。
-
程序错误如:
零作除数、非法存储访问等,这种情况通常是由硬件而不是由Linux内核检测到的,但由内核向发生此错误的那个进程发送相应的信号; -
外部事件如:
当用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时,内核会生成一个信号通知进程; -
显式请求如:
使用kill()函数允许进程发送任何信号给其他进程或进程组。
信号的生成既可以是同步的,也可以是异步的。
同步信号大多数是程序执行过程中出现了某个错误而产生的 或 由进程显式请求生成的给自己的信号等。
异步信号是接收进程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。
一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,
进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。
4.5 信号处理
无论是同步还是异步信号,当信号发生时,我们可以告诉Linux内核采取如下3种动作中的任意一种:
-
忽略信号。
大部分信号都可以被忽略,但有两个除外:SIGSTOP和SIGKILL绝不会被忽略。
不能忽略这两个信号的原因是为了给超级用户提供杀掉或停止任何进程的一种手段。
此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。
例如,若忽略硬件例外(非法指令)信号, 则会导致进程的行为不确定。 -
捕获信号。
这种处理是要告诉Linux内核,当信号出现时调用专门提供的一个函数。
这个函数称为信号处理函数,它专门对产生信号的事件作出处理。 -
让信号默认动作起作用。
系统为每种信号规定了一个默认动作,这个动作由Linux内核来完成, 有以下几种可能的默认动作:
1. 终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为core的文件中;
2. 终止终止进程但不生成core文件。
3. 忽略信号。
4. 暂停进程。
5. 若进程是暂停暂停,恢复进程,否则将忽略信号。
4.6 信号捕获
很多时候我们使用信号只是通知进程而不是要杀死它,或者在杀死它前我们想进行某些收尾工作, 这个时候就是需要我们去捕获这个信号,然后去处理它。
在Linux中,捕获信号的函数有很多, 比如signal()、sigaction()等函数。
-
signal()
signal()主要是用于捕获信号,可以改变进程中对信号的默认行为,
我们在捕获这个信号后,也可以自定义对信号的处理行为,当收到这个信号后,应该如何去处理它, 这也是我们在开发Linux最常使用的方式。使用signal()时,它需要提前设置一个回调函数,
即进程接收到信号后将要跳转执行的响应函数, 或者设置忽略某个信号,才能改变信号的默认行为,这个过程称为“信号的捕获”。
对一个信号的“捕获”可以重复进行,不过signal()函数将会返回前一次设置的信号响应函数指针。我们可以使用man命令去查看signal()相关介绍,可查询到其原型如下:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 参数: signum:指出准备捕获或忽略的信号 如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。 handler:指出接收到指定的信号后将要调用的函数 它的类型是 void(*sighandler_t)(int) 类型,拥有一个int类型的参数, 这个参数的作用就是传递收到的信号值,返回类型为void。 handler需要用户自定义处理信号的方式,当然还可以使用以下宏定义: SIG_IGN:忽略该信号。 SIG_DFL:采用系统默认方式处理信号。 返回值: sighandler_t类型的函数指针 这是因为调用signal()函数修改了信号的行为,需要返回之前的信号处理行为是哪个,以便让应用层知悉, 如果修改信号的默认行为识别则返回对应的错误代码SIG_ERR。
如果调用处理程序导致信号被阻塞,则从处理程序返回后,信号将被解除阻塞。无法捕获或忽略信号SIGKILL和SIGSTOP。
-
sigaction()
其实,我们不推荐读者使用signal()函数接口,之所以会在上一小节介绍它, 是因为读者可能会在许多老程序中看到它的应用,而且相对简单。
稍后我们会介绍一个定义更清晰、执行更可靠的sigaction()函数,这个函数的功能与signal()函数是一样的,但是API接口稍微有点不同,
我们建议以后在所有的程序中都应该使用这个函数去操作信号。sigaction()函数原型如下:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 参数区别如下: signum: 指定捕获的信号值。 act: 是一个结构体,该结构体的内容如下: struct sigaction { /* 捕获信号后的处理函数,它也有一个int类型的参数,传入信号的值,这个函数是标准的信号处理函数 */ void (*sa_handler)(int); /* 扩展信号处理函数 */ /* 事实上如果选择扩展接口的话,信号的接收进程不仅可以接收到int型的信号值,还会接收到一个 siginfo_t类型的结构体指针,还有一个void类型的指针 */ /* 还有需要注意的就是,不要同时使用sa_handler和sa_sigaction,因为这两个处理函数是有联合的部分(联合体) */ void (*sa_sigaction)(int, siginfo_t *, void *); /* 信号掩码,它指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号,在进程响应信号期间被临时阻塞 */ /* 除非使用SA_NODEFER标志,否则即使是当前正在处理的响应的信号再次到来的时候也会被阻塞 */ sigset_t sa_mask; /* 指定一系列用于修改信号处理过程行为的标志,由下面的0个或多个标志组合而成 SA_NOCLDSTOP: 如果signum是SIGCHLD,则在子进程停止或恢复时,不会传信号给调用sigaction()函数的进程。 即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一种时或接收到SIGCONT(恢复)时,父进程不会收到通知。 仅当为SIGCHLD建立处理程序时,此标志才有意义。 SA_NOCLDWAIT: 它表示父进程在它的子进程终止时不会收到SIGCHLD 信号,这时子进程终止则不会成为僵尸进程 SA_NODEFER: 不要阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效, 即在信号处理函数执行期间仍能接收这个信号 仅当建立信号处理程序时,此标志才有意义 SA_RESETHAND: 信号处理之后重新设置为默认的处理方式。 SA_SIGINFO: 指示使用sa_sigaction成员而不是使用sa_handler 成员作为信号处理函数。 当在sa_flags中指定SA_SIGINFO标志时,信号处理程序地址将通过sa_sigaction字段传递。该处理程序采用三个参数,如下所示: void handler(int sig, siginfo_t *info, void *ucontext) { ... } info指向siginfo_t的指针,它是一个包含有关信号的更多信息的结构,具体成员变量如下所示: typedef struct { int si_signo; // SI_USER/QUEUE/TIMER/ASYNCIO/MEGEQ int si_code; union sigval si_value; } siginfo_t 上面的成员变量绝大部分我们是几乎使用不到的, 因为我们如果是对信号的简单处理,直接使用sa_handler处理即可, 根本无需配置siginfo_t这些比较麻烦的信息。 */ int sa_flags; /* 废弃的成员变量,不要使用 */ void (*sa_restorer)(void); }; oldact: 返回原有的信号处理参数,一般设置为NULL即可
对应例程如下
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> /* 信号处理函数 */ /* 这里并没有在函数中让信号恢复默认处理,这是因为设置了sa_flags成员变量,在处理完信号后自动恢复默认的处理 */ void signal_handler(int sig) { printf("\nthis signal number is %d \n",sig); if (sig == SIGINT) { printf("I have get SIGINT!\n\n"); printf("The signal is automatically restored to the default handler!\n\n"); /* 信号自动恢复为默认处理函数 */ } } int main(void) { struct sigaction act; printf("this is sigaction function test demo!\n\n"); /** 设置信号处理的回调函数 */ act.sa_handler = signal_handler; /* 清空屏蔽信号集,即在信号处理的时候不会屏蔽任何信号 */ sigemptyset(&act.sa_mask); /* 在处理完信号后恢复默认信号处理 */ act.sa_flags = SA_RESETHAND; /* 捕获SIGINT信号 */ sigaction(SIGINT, &act, NULL); while (1) { printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n"); sleep(1); } exit(0); }
4.7 信号发送
-
kill()
kill命令的语法如下:kill [信号或选项] PID(s)
如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill命令。
该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的PID(这个PID一般需要用ps命令查出来),
例如,如果要向运行在另一个终端上的PID为666的进程发送“挂断”信号(SIGHUP),可以使用如下命令:kill - SIGHUP 666 / kill -1 666(这里的-1是指信号值为1 的SIGHUP信号)
kill()函数与kill系统命令一样,可以发送信号给进程或进程组,实际上,kill系统命令只是kill()函数的一个用户接口。
这里需要注意的是,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。头文件: #include <sys/types.h> #include <signal.h> 函数原型: int kill(pid_t pid, int sig); 参数: pid pid的取值如下: pid > 1:将信号sig发送到进程ID值为pid指定的进程。 pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。 pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外。 pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程。 sig: 要发送的信号值。 函数返回值: 0:发送成功。 -1:发送失败并设置errno变量。 失败的原因可能是: 给定的信号无效(errno设置为INVAL) 发送进程权限不够(errno设置为EPERM) 目标进程不存在(errno设置为ESRCH)等情况。
进程可以通过调用kill()函数向包括它本身在内的其他进程发送一个信号。
如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。
因此要想发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户ID
(即你只能发送信号给属于自己的进程, 但超级用户可以发送信号给任何进程)。 -
raise()
raise()函数也是发送信号函数,不过与 kill()函数所不同的是,raise()函数只是进程向自身发送信号的,而没有向其他进程发送信号,
可以说 kill(getpid(), sig) 等同于 raise(sig)。int raise(int sig); 参数sig: 代表着发送的信号值 函数返回值: 0:发送成功。 -1:发送失败并设置errno变量。 发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况。 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> int main(void) { pid_t pid; int ret; /* 创建一子进程 */ if ((pid = fork()) < 0) { printf("Fork error\n"); exit(1); } if (pid == 0) { /* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */ printf("Child(pid : %d) is waiting for any signal\n\n", getpid()); /* 子进程给自己发暂停命令,暂停在这里 */ raise(SIGSTOP); exit(0); } else { /* 等待一下,等子进程先执行 */ sleep(1); /* 在父进程中收集子进程发出的信号(不阻塞),并调用 kill()函数进行相应的操作 */ /* WNOHANG: 子进程没退出,则直接返回0,不等待 */ if ((waitpid(pid, NULL, WNOHANG)) == 0) { /* 子进程还没退出,返回为0,就发送SIGKILL信号杀死子进程 */ if ((ret = kill(pid, SIGKILL)) == 0) { printf("Parent kill %d\n\n",pid); } } /* 使用waitpid()函数回收子进程资源,一直阻塞直到子进程退出(杀死) */ /* 父进程是怎么回收子进程资源的?只要在子进程挂掉之前父进程还在就能自动回收? */ waitpid(pid, NULL, 0); exit(0); } }
-
alarm()
alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间seconds到时, 它就向进程发送SIGALARM信号。
其函数原型如下:
unsigned int alarm(unsigned int seconds);如果在seconds秒内再次调用了alarm()函数设置了新的闹钟,则新的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代。
它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。
特别地,如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。验证SIGALRM信号的默认处理
int main() { printf("\nthis is an alarm test function\n\n"); alarm(5); sleep(20); printf("end!\n"); return 0; }
实际上这个程序只是定义了一个时钟alarm(5),它的作用是让SIGALRM信号在经过5秒后传送给目前main()所在进程;
接着又调用了sleep(20)让进程睡眠20秒的时间。
当main()程序挂起5秒钟后,alarm产生了SIGALRM信号,由于我们没有做捕获处理,系统会调用该信号的默认处理函数,
即执行exit(0)函数直接终止进程,并且在终止的时候自动打印”Alarm clock”(闹钟)。由于执行默认处理函数后进程终止,代码自身的最后一句printf(“end!n”)代码是不会被执行的。
alam()函数覆盖配置实验
int main() { unsigned int seconds; printf("\nthis is an alarm test function\n\n"); seconds = alarm(20); printf("last alarm seconds remaining is %d! \n\n", seconds); printf("process sleep 5 seconds\n\n"); sleep(5); printf("sleep woke up, reset alarm!\n\n"); seconds = alarm(5); printf("last alarm seconds remaining is %d! \n\n", seconds); sleep(20); printf("end!\n"); return 0; }
这个alarm测试代码是为了验证多次设置alarm的时候,它会覆盖前一次的设置值。
代码的逻辑非常简单,首先调用alarm(20)函数设置在20秒后产生一个SIGALRM信号, 进程睡眠5秒后唤醒,再次设置alarm(5)函数在5秒后产生SIGALRM信号终止进程,
此时上一个alarm设置就被覆盖了,并且返回上一次设置的剩余的时间(15秒),覆盖配置后,进程还需要睡眠, 等待5秒后SIGALRM信号的到来。
五、总结
- 信号(signal),又称为软中断信号,用于通知进程发生了异步事件
- 信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
- 一般而言,信号的响应处理过程如下:
如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。
如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;
如果设置为忽略,那么直接丢弃该信号。
最后才执行信号的默认处理。 - 非实时信号不支持排队, 因此多次收到相同信号可能会丢失
实时信号支持排队的,发送了多少个信号给进程,进程就会处理多少次 - 一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!
一般是在中断返回的时候,或者内核态返回用户态的时候(这种情况是比较常见的处理方式)。 - 注册信号回调函数:signal(SIGINT, signal_handler);
使用宏定义处理信号:默认方式:signal(SIGINT, SIG_DFL); / 忽略处理:signal(SIGINT, SIG_IGN);