这段时间因为别的组在用我们代码的时候发现ctrl+c无法使进程退出,表现出来的现象是我们的init函数有问题,只要加上我们的init函数,那么遇到ctrl+c的操作时,进程就是无法退出。最终通过gdb调试,发现是另一个组的基于shm和udp的消息发布订阅模块造成的,但是因为没有源码,只能定位到是析构函数有问题。因为开发人员休假无法到位,于是趁此机会把跨进程的信号通信机制了解了一下。毕竟以前都是在书上看的,纸上得来终觉浅。
1. 信号是啥
它是一种特殊的IPC(进程间通信)机制,它是系统里面已经设计好了的,我们只能去使用它,且是一种异步通信方式。系统里面自带的信号可以用kill -l列出来,总共64个。
- 非实时信号:前面31种信号值是不可靠信号值(非实时信号),同时发送多个一样的信号只会响应第一个信号,后面的会忽略掉;
- 实时信号:后面33是后来添加的是可靠的信号值(实时信号值) ,无论发送多少个一样的信号都会逐个响应
- 信号间可以嵌套,但是会按接收到的顺序逐一响应,接收到多个相同的不可靠信号(1-31号信号)只会执行一个
比较常用的信号:
- 2号SIGINT:终止一个进程(ctrl + c);
- 9号SIGKILL:强制杀死一个进程;
- 18号SIGCONT:回复一个进程的运行(之前已经执行了 SIGSTOP 这个暂停信号);
- 19号SIGSTOP:暂停一个进程的运行,并没有结束掉
2. kill指令
kill和killall都是用来发信号给某个(些)进程的,和我们原来理解的“杀死”有点区别的,不能光看到kill -9 就以为kill是杀死进程的意思哈。
- kill -sigNum pid
- killall -sigNum procName
注:可先使用ps -ef查看目前系统正在运行的所有进行及其进程号等进程信息
kill除了是系统命令外,还是系统提供的函数。用户可以在函数中使用。
函数原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函数形参:
pid:想要用于接收发送出去的信号的进程pid号
>0:将信号发送给指定的进程;
=0:将信号发送给当前的进程组;
=-1:将信号发送给每一个有权限接收这个信号的进程;
<-1:这个pid=某个进程组的ID取反(-12345)
sig:需要发送的信号值,0表示不发送任何信号
函数返回值:
成功返回0
失败返回-1
示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
//创建一个子进程
pid_t id = fork();
if(id<0)
{
perror("fork failed");
return -1;
}
else if(id==0)
{
printf("I am %d\n",getpid());
sleep(10);
//把自己杀掉
kill(getpid(),9);
printf("i am child and pid = %d\n",getpid());
}
else
{
printf("I am parent, pid= %d\n",getpid());
sleep(20);
}
//退出程序并且刷新缓冲区
exit(0);
}
可以看到子进程自己把自己杀死了,并变成了“僵尸进程(defunct)”,后面的语句无法输出。之后等待10s后,父进程退出,系统回收资源,进程资源释放,无法再显示这两个进程。
3. 信号中的signal等函数
进程对信号的操作主要有:
- 缺省:执行默认动作
- 捕捉:接收到信号后,执行自己规定的动作
- 忽略:接收到信号后,不执行任何操作
- 阻塞:接收到信号后,不立即响应,等该信号被解除阻塞,再响应(延迟响应)
注:9,18,19这几个信号不可捕捉,忽略,阻塞;否则所有进程都忽略了这几个函数所有进程将变成守护进程,当进程创建到达最大数时将会导致系统奔溃
(1)捕捉与忽略信号函数signal
函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数形参:
typedef void (*sighandler_t)(int):宏定义了一个函数指针,返回值类型为void,形参类型为:int
signum:需要捕捉的信号值
handler:本质是一个指针函数,接受接收到相应信号执行的函数,也可使用:
SIG_IGN —》忽略
SIG_DFL —》执行默认动作
函数返回值:
成功:返回前一个这个信号处理的回调函数的指针
出错:出现错误时,返回SIG_ERR
当signal(int signum, sighandler_t handler)的handler设置为:
- SIG_IGN时接收到相对应的信号时将不执行任何动作
- SIG_DFL时接收到相对应的信号时将执行系统默认的动作(和正常使用效果一样)
- 其他函数时接收到相对应的信号时按照设定的函数执行动作
示例:
这里实际上是更改了系统设计的信号值为2、3、4的处理函数,也就是当程序遇到2、3、4信号值时,不再使用默认的处理方式。例如2号sigINT,也就是ctrl+c。如果我们按下ctrl+c或者执行kill -2 pid,这两个操作是否还是像往常一样把进程杀死呢?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void handel(int num)
{
if(num == 2)
{
printf("catch the 2 signal\n");
}
else if(num == 3)
{
printf("catch the 3 signal\n");
}
else if(num == 4)
{
printf("catch the 4 signal\n");
}
}
int main()
{
//捕捉2,3,4号信号,并且执行相对应的函数
signal(2,handel);
signal(3,handel);
signal(4,handel);
//创建一个子进程
while(1)
{
printf("hello world\n");
sleep(1);
}
//退出程序并且刷新缓冲区
exit(0);
}
下面我们在程序运行过程中使用ctrl+c试一下,也就是左图中的^Ccatch the 2 signal那一行。我们发现进程并不退出,说明我们使用ctrl+c和kill -2 效果一样。
(2)阻塞相关信号函数:
阻塞时,该信号不执行,等待解除阻塞再执行该信号,并且如果是1-31号的信号,发送多个相同信号也只会执行一次。常用的阻塞信号函数原型:
//一个信号列表(集合)
sigset_t set;
//信号值
int signum;
//清空一个信号列表(集合)
int sigemptyset(sigset_t *set);
//把所有的信号都加入一个信号列表(集合)
int sigfillset(sigset_t *set);
//把某一个信号都加入一个信号列表(集合)
int sigaddset(sigset_t *set, int signum);
//把某一个信号从一个信号列表(集合)删除
int sigdelset(sigset_t *set, int signum);
//判断一个信号是否在一个信号列表(集合)
int sigismember(const sigset_t *set, int signum);
//把一个信号列表(集合)设置为阻塞状态或解除阻塞
// how:(1)SIG_BLOCK:在原有的阻塞信号中再添加信号列表(集合)中的信号
// (2)SIG_UNBLOCK:在原有阻塞信号基础上(在阻塞信号中找)解除信号列表(集合)中的信号
// (3)SIG_SETMASK:把原有的阻塞信号全部替换成信号列表(集合)中的信号
//
//const sigset_t *set:新的信号列表(集合)
//sigset_t *oldset:原有的信号列表(集合)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数的返回值:
成功:sigismember返回1,其他函数均为返回0
失败:sigismember返回0,其他函数均为返回-1
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
//定义一个信号列表(集合)
sigset_t set;
//清空一下信号列表(集合)
sigemptyset(&set);
//添加信号到信号列表
sigaddset(&set,2);
sigaddset(&set,3);
sigaddset(&set,4);
//设置信号列表为阻塞
sigprocmask(SIG_SETMASK, &set, NULL);
int i=0;
while(1)
{
//判断i是否为20,如果是就解除阻塞
if(i==20)
{
//解除信号阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
//最后看会不会打印这行
printf("hello world\n");
sleep(1);
//每次加1
i++;
}
//退出程序并且刷新缓冲区
exit(0);
}
另外,误操作学到一个高级操作,在本目录下搜索字符串:
带上通配符能搜字符串匹配的内容,不带通配符就是列出进程啦。