4. 信号
4.1 进程间通信概述
进程间通信
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。
进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信 (IPC,Inter Processes Communication)。
进程间通信功能:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变
linux进程间通信(IPC)由以下几个部分:
- UNIX 进程间通信
- SYSTEM V进程间通信
- POSIX 进程间通信(POSIX:Portable Operating System interface 可移植操作系统接口)
- Socket 进程间通信
进程间通信的实质
系统只要创建一个进程,就会给当前进程分配4G的虚拟内存(32位操作系统),虚拟内存不是常说的内存条的空间,内存条的空间称之为物理内存,虚拟内存和物理内存之间存在映射关系,4G的虚拟内存分为3G的用户空间(0-3G)和1G(3-4G)的内核空间
用户空间是进程所私有的,每一个进程的用户空间只能自己访问和使用,我们之前说的栈区、堆区数据区、代码区等都是用户空间的区域
内核空间是所有进程所公有的,也就意味着绝大多数进程间通信方式,本质就是对内核空间的操作
特殊的进程间通信方式
socket通信可以实现不同主机的进程间通信,其他六个只能在一台主机的多个进程间通信信号通信是唯一的一种异步通信机制
共享内存是所有进程间通信方式中效率最高的,他是直接对物理内存进行操作
4.2 信号的概述
4.2.1概述
信号是软件中断,它是在软件层次上对中断机制的一种模拟
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
信号是一种异步通信方式
进程不必等待信号的到达,进程也不知道信号什么时候到达。信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。
每个信号的名字都以字符 SIG 开头。
每个信号和一个数字编码相对应,在头文件 signumh中,这些信号都被定义为正整数。
信号名定义路径(ubuntu 20.04 LTS):
usr/include/x86_64-linux-gnu/bits/signum.h
在Linux下,要想查看这些信号和编码的对应关系,可使用命令:kill-l
$ 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
信号是由当前系统已经定义好的一些标识,每一个标识都会在特定的场合使用并且都会对进程有一定的影响;
当信号产生时,会让当前信号做出相应的操作;
这些信号都是已经定义好的,我们不能自己再去创造,直接使用这些就可以;
4.2.2 产生信号的方式
-
当用户按某些终端键时,将产生信号。例如:
1. 终端上按 Ctl+C 组合键通常产生中断信号 SIGINT 2. 终端上按 Ctr+\ 键通常产生中断信号 SIGQUIT 3. 终端上按 Ctr1+Z 键通常产生中断信号 SIGSTOP
-
硬件异常将产生信号
除数为 0,无效的内存访问等, 这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程
-
软件异常将产生信号
当检测到某种软件条件已经发生,并将其通知有关进程时,产生信号
-
调用
kill
函数将发送信号注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户
-
运行
kill
命令将发送信号此程序实际上是使用 kill 函数来发送信号,也常用此命令终止一个失控的后台进程
4.2.3 信号的默认(缺省)处理方式
当进程中产生了一个信号,就会让当前进程做出一定的反应默认处理进程的方式如下
1、终止进程:当信号产生后,当前进程就会立即结束
2、缺省处理:当信号产生后,当前进程不做任何处理
3、停止进程:当信号产生后,使得当前进程停止
4、让停止的进程回复运行:当信号产生后,停止的进程会回复执行(后台进程)
注意:每一个信号只有一个默认的处理方式
4.2.4 进程接收到信号后的处理方式
1、执行系统默认动作
对大多数信号来说,系统默认动作是用来终止该进程
2、忽略此信号
接收到此信号后没有任何动作
3、执行自定义信号处理函数
用用户定义的信号处理函数处理该信号
注意: SIGKILL
和SIGSTOP
这两个信号只能以默认的处理方式执行
4.2.5 常见的信号
信号 | 值 | 性质 | 默认处理方式 |
---|---|---|---|
SIGKILL | 9 | 产生这个信号,当前进程会退出,不能被缺省和捕捉 | 退出进程 |
SIGSTOP | 19 | 产生这个信号,当前进程会停止,不能被缺省和捕捉 | 停止进程 |
SIGINT | 2 | 键盘CTRL + C | 退出进程 |
SIGQUIT | 3 | 键盘CTRL + \ | 退出进程 |
SIGTSTP | 20 | 键盘CTRL + Z | 退出进程 |
SIGCONT | 18 | 当产生该信号后,当前停止的进程会恢复 | 停止进程恢复运行 |
SIGALRM | 14 | 当调用alarm函数设置的时间到达后产生该信号 | 退出进程 |
SIGPIPE | 13 | 当管道破裂时,会产生当前信号 | 退出进程 |
SIGABRT | 6 | 调用abort函数时,产生当前信号 | 退出进程 |
SIGCHLD | 17 | 当使用fork创建子进程时,如果子进程状态改变,会产生该信号 | 缺省 |
SIGUSR1 | 10 | 用户自定义信号,不会自动产生,只能用kill 函数或者命令指定进程发送该信号 |
缺省 |
SIGUSR2 | 12 | 用户自定义信号,不会自动产生,只能用kill 函数或者命令指定进程发送该信号 |
缺省 |
4.3 kill函数
kill 向一个进程发送信号
#include <sys/types.h>
#include <signal.h>
int kill(pid t pid, int signum);
功能:
给指定进程发送信号参数;
pid: 详见下页
signum: 信号的编号;
返回值:
成功返回 0,失败返回 -1。
pid的取值有 4种情况:
pid>0: 将信号传送给进程 ID 为 pid 的进程。
pid=0: 将信号传送给当前进程所在进程组中的所有进程
pid=-1: 将信号传送给系统内所有的进程
pid<-1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid的绝对值
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{ // 父进程
while (1)
{
printf("this is a parent process\n");
sleep(1);
}
}
else
{ // 子进程代码区
printf("this is a son process\n");
// 子进程在3秒以后,让父进程退出
sleep(3);
// 使用kill给父进程发送信号,父进程接收到信号后直接退出就可以了
kill(getppid(), SIGKILL);
}
return 0;
}
输出结果
this is a son process
this is a parent process
this is a parent process
this is a parent process
已杀死
4.4 alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
在 seconds 秒后,向调用进程发送一个 SIGALRM 信号,SIGALRM 信号的默认动作是终止调用 alarm 函数的进程;
返回值:
若以前没有设置过定时器,或设置的定时器己超时返回 0;
否则返回定时器剩余的秒数,并重新设定定时器。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
unsigned int sec;
// 当执行到alarm之后,代码会接着往下执行,当设定的时间到后,会产生SIGALRM信号
//如果alarm之前没有设置其他闹钟,则返回8,如果之前设置了,则返回之前剩余的秒数
//如果一个程序中出现多个alarm闹钟,第一个如果没有到达指定的时间就遇到第二个
//则第一个的闹钟时间清除,按照第二个alarm闹钟的时间继续向下运行
sec = alarm(5);
printf("sec = %d\n", sec);
sleep(3);
sec = alarm(6);
printf("sec = %d\n", sec);
while (1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
输出结果
sec = 0
sec = 2
hello world
hello world
hello world
hello world
hello world
hello world
闹钟
4.5 raise函数
#include<signal.h>
int raise(int sig);
功能:给调用进程本身发送信号;
参数:
sig:指定的信号;
返回值:
成功: 0
失败: 非0
raise(sig) <==> kill(getpid(),sig)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int num = 0;
while (1)
{
printf("hello world\n");
sleep(1);
num++;
if (num == 5)
{
// 当循环执行5秒后,进程退出
// 使用raise给当前进程本身发送信号
// 此处亦可使用
// kill(getpid(),SIGALRM);
raise(SIGALRM);
}
}
return 0;
}
hello world
hello world
hello world
hello world
hello world
闹钟
4.6 abort 函数
#include <stdlib.h>
void abort(void);
功能:
向进程发送一个 SIGABRT 信号,默认情况下进程会退出。
注意:
即使 SIGABRT 信号被加入阻塞集,一旦进程调用了 abort 函数,进程也还是会被终止,且在终止前会刷新缓冲区,关闭文件描述符。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int num = 0;
while (1)
{
printf("hello world\n");
sleep(1);
num++;
if (num == 5)
{
// 当循环执行5秒后,进程退出
// 使用 abort 终止进程
abort();
}
}
return 0;
}
输出结果
hello world
hello world
hello world
hello world
hello world
已放弃 (核心已转储)
4.7 pause函数
#include <unistd.h>
int pause(void);
功能:
将调用进程挂起直至捕捉到信号为止, 这个函数通常用于判断信号是否已到;
参数: 无
返回值:
直到捕获到信号,pause 函数才返回-1,且errno被设置成 EINTR
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{ // 父进程
printf("this is a parent process\n");
// 使用pause函数阻塞,等待信号
pause();
}
else
{ // 子进程代码区
printf("this is a son process\n");
// 子进程在3秒以后,让父进程退出
sleep(3);
// 使用kill给父进程发送信号,父进程接收到信号后直接退出就可以了
kill(getppid(), SIGKILL);
}
}
输出结果
this is a parent process
this is a son process
已杀死
4.8 signal函数
进程接收到信号后的处理方式
-
执行系统默认动作
-
忽略此信号
-
执行自定义信号处理函数
程序中可用函数 signal()
改变信号的处理方式
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
typedef void (*sighander_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址;
参数:
signum: 信号编号
handler 的取值:
忽略该信号:SIG_IGN
执行系统默认动作:SIG_DFL
自定义信号处理函数:信号处理函数名
返回值:
成功:返回函数地址,该地址为此信号上一次注册的信号处理函数的地址;
失败:返回 SIG_ERR
4.8.1 signal函数的使用
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void handler(int sig);
int main(int argc, char const *argv[])
{
// 1. 以默认的方式处理信号
#if 0
if (signal(SIGINT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif
// 2. 以忽略的方式处理信号 SIG_IGN
#if 0
if (signal(SIGINT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif
// 3. 以用户自定义方式处理信号 handler
#if 1
if (signal(SIGINT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif
while (1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
void handler(int sig)
{
if (sig == SIGINT)
{
printf("SIGINT正在处理\n");
}
if (sig == SIGQUIT)
{
printf("SIGQUIT正在处理\n");
}
if (sig == SIGSTOP)
{
printf("SIGSTOP正在处理\n");
}
}
4.8.2 signal函数的返回值
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void *ret_handler;
void handler(int sig)
{
printf("**************************\n");
printf("hello world\n");
printf("welcome to beijing\n");
printf("**************************\n");
if (signal(SIGINT, ret_handler) == SIG_ERR)
{
perror("fail to signal\n");
exit(1);
}
}
int main(int argc, char const *argv[])
{
if ((ret_handler = signal(SIGINT, handler)) == SIG_ERR)
{
perror("fail to signal\n");
exit(1);
}
while (1)
{
printf(" I love C\n");
sleep(1);
}
return 0;
}
输出结果
I love C
I love C
I love C
^C**************************
hello world
welcome to beijing
**************************
I love C
I love C
^C
第一次按ctrl + c
,触发信号中断函数,此次以自定义的方式处理,执行handler
函数,返回值为ret_handler
。 返回值是当前信号上一次的处理方式,此时的ret_handler
是SIGINT的默认方式
;
再次按ctrl + c
,就会使用默认的方式,中断进程并退出。
4.8.3 可重入函数
可重入函数是指函数可以由多个任务并发使用,而不必担心数据错误;
可重入函数就是可以被中断的函数,当前函数可以在任何时刻中断它,并执行另一块代码当执行完毕后,回到原本的代码还可以正常继续运行。
编写可重入函数:
- 不使用(返回)静态的数据、全局变量(除非用信号量互斥)
- 不调用动态内存分配、释放的函数
- 不调用任何不可重入的函数(如标准I/0函数)
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int sig)
{
printf("SIGINT\n");
}
int main(int argc, char const *argv[])
{
signal(SIGINT, handler);
#if 1
// sleep(5); // 按下ctrl+c 会中断函数,其是可重入函数,但是由于其本身性质,就会中断;
alarm(5); // alarm是可重入函数,当它执行的时候,如果有其他信号传入并执行处理函数,执行完毕后,会继续回到alarm函数
while (1)
{
printf("hello world\n");
sleep(1);
}
#endif
return 0;
}
输出结果
hello world
hello world
^CSIGINT
hello world
hello world
hello world
hello world
闹钟
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int sig)
{
printf("SIGINT\n");
}
int main(int argc, char const *argv[])
{
signal(SIGINT, handler);
#if 0
// sleep(5); // 按下ctrl+c 会中断函数,其是可重入函数,但是由于其本身性质,就会中断;
alarm(5); // alarm是可重入函数,当它执行的时候,如果有其他信号传入并执行处理函数,执行完毕后,会继续回到alarm函数
while (1)
{
printf("hello world\n");
sleep(1);
}
#endif
#if 1
char buf[32] = "";
if (read(0, buf, 20) == -1)
// read也是可重入函数,在等待终端输入时,如果产生信号并执行信号处理函数,
// 信号处理函数执行完毕后,可继续输入数据,read读取信号处理函数之后的数据
{
perror("fail to read\n");
exit(1);
}
printf("buf = [%s]\n", buf);
#endif
return 0;
}
输出结果
123^CSIGINT
asd^CSIGINT
fgh
buf = [fgh
]
4.9 信号集
4.9.1 信号集概述
一个用户进程常常需要对多个信号做出处理。为了方便对多个信号进行处理,在 Linux 系统中引入了信号集。信号是用来表示多个信号的数据类型。
4.9.2 信号集数据类型
sigset_t
4.9.3 信号集相关操作
sigemptyset; // 初始化空的信号集
sigfillset; // 初始化满的信号集
sigismember; // 判断信号是否为信号集成员
sigaddset; // 添加一个信号到信号集
sigdelset; // 删除信号集的一个信号
sigemptyset()
#include<signal.h>
int sigemptyset(sigset t*set);
功能:
初始化由set指向的信号集,清除其中所有的信号即初始化一个空信号集;
参数:
set:信号集标识的地址,以后操作此信号集,对set进行操作就可以了;
返回值:
成功返回 0
失败返回 -1
// --------------------------------------------------------------------
sigfillset()
#include <signal.h>
int sigfillset(sigset t *set);
功能:
初始化信号集合set,将信号集合设置为所有信号的集合;
参数:
信号集标识的地址,以后操作此信号集,对set进行操作就可以了;
返回值:
成功返回 0
失败返回 -1
// --------------------------------------------------------------------
sigismember()
#include <signal.h>
int sigismember(const sigset t*set,int signum);
功能:
查询signum标识的信号是否在信号集合set之中;
参数:
set:信号集标识符号的地址。
signum:信号的编号。
返回值:
成功:在信号集中返回1,不在信号集中返回0;
错误,返回-1
// --------------------------------------------------------------------
sigaddset()
#include<signal.h>
int sigaddset(sigset t*set, int signum);
功能:
将信号signum加入到信号集合set之中。
参数:
set:信号集标识的地址。
signum:信号的编号;
返回值:
成功返回 0
失败返回 -1
// --------------------------------------------------------------------
sigdelset()
#include<signal.h>
int sigdelset(sigset t*set,int signum);
功能:
将signum所标识的信号从信号集合set中删除;
参数:
set:信号集标识的地址。
signum:信号的编号;
返回值:
成功:返回 0
失败:返回 -1
#include <signal.h>
#include <stdio.h>
#include <bits/types/sigset_t.h>
int main(int argc, char const *argv[])
{
sigset_t set;
int ret = 0;
// 初始化空的信号集
sigemptyset(&set);
// 判断SIGIN信号是否在信号集中
ret = sigismember(&set, SIGINT);
if (ret == 0)
{
printf("SIGINT is not a number of sigprocmask \nret = %d\n", ret);
}
// 将指定的信号添加到集合中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 再次判断SIGIN信号是否在信号集中
ret = sigismember(&set, SIGINT);
if (ret == 0)
{
printf("SIGINT is not a number of sigprocmask \nret = %d\n", ret);
}
else
{
printf("SIGINT is a number of sigprocmask \nret = %d\n", ret);
}
return 0;
}
输出结果
SIGINT is not a number of sigprocmask
ret = 0
SIGINT is a number of sigprocmask
ret = 1
4.9.4 信号阻塞集(屏蔽集、掩码)
每个进程都有一个阻塞集,它用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住直到进程准备好时再将信号通知进程)。
所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
#include<signal.h>
int sigprocmask(int how,const sigset t *set, sigset t *oldset);
功能:检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞由set指定,而原先的信号阻塞集合由oldset保存;
参数:
how:信号阻塞集合的修改方法:
SIG_BLOCK:向信号阻塞集合中添加set信号集;
SIG_UNBLOCK:从信号阻塞集合中删除set信号集;
SIG_SETMASK:将信号阻塞集合设为set集合
set:要操作的信号集地址。
oldset:保存原先信号集地址。
注:若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中;
返回值:
成功:返回0
失败:返回 -1
#include <signal.h>
#include <stdio.h>
#include <bits/types/sigset_t.h>
#include <unistd.h>
#include <stdlib.h>
#include <bits/sigaction.h>
int main(int argc, char const *argv[])
{
int i = 0;
// 创建信号集
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
while (1)
{
// 将set信号集添加到信号阻塞集中
sigprocmask(SIG_BLOCK, &set, NULL);
for (i = 0; i < 5; i++)
{
printf("SIGINT signal is blocked\n");
sleep(1);
}
// 将set信号集从信号阻塞集中删除
sigprocmask(SIG_UNBLOCK, &set, NULL);
for (i = 0; i < 5; i++)
{
printf("SIGINT signal is unblocked\n");
sleep(1);
}
}
return 0;
}
输出结果
SIGINT signal is blocked
SIGINT signal is blocked
^CSIGINT signal is blocked
SIGINT signal is blocked
SIGINT signal is blocked
在被阻塞的时候,ctrl+c
不好使,等解除阻塞,会执行ctrl+c
,中断程序。