linux之信号操作
sigset_t
这是信号在内核中的表示
==block和pending都是位图——即用bit位来表示信号编号!==
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用==相同的数据类型sigset_t==来存储,==sigset_t称为信号集==,这个类型可以表示每个信号 的“有效”或“无效”状态(在软件层面上就是将bit为从0置为1,或者从1置为0,硬件上就是从充放电来表示)
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释(如用printf直接打印sigset_t变量是没有意义的)
sigprocmask
==这个函数的作用就是用来更该进程的block表!==——那个进程调用就修改那个进程!
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
sigpengding
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
我们接下来可与用上面的函数做一些实验!
我们知道,一般情况:我们所有的信号都是不被阻塞的!如果一个信号被阻塞,那么该信号就不会被抵达!如果收到了为阻塞的信号,那么就会被存入pending位图里面!
#include<iostream>
#include <signal.h>
#include<string>
#include<vector>
#include<unistd.h>
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
static std::vector<int> sigv = {2};//想要屏蔽那些信号往这个数组加就可以了!
static void show_pending(const sigset_t& pending)
{
std::string show;
for(int i = MAX_SIGNUM;i>=1;i--)
{
if(sigismember(&pending,i) == 1)
show +='1';
else
show +='0';
}
std::cout << show << std::endl;
}
int main()
{
//首先尝试屏蔽2号信号!
sigset_t set,oset,pending;
//1.1初始化!
sigemptyset(&set);//全部设为0
sigemptyset(&oset);
sigemptyset(&pending);
//1.2添加要屏蔽的信号
for(auto e:sigv)
{
sigaddset(&set, e); // 在set信号集将e号信号色设置为1
}
//前面这一堆动作是没有影响该进程信号屏蔽字的!——只是在用户层构建一个信号集!
//1.3屏蔽信号!
sigprocmask(SIG_BLOCK,&set,&oset);//将信号集设置进内核!
//遍历打印所有的pending信号集!
while(true)
{
//2.1获取
sigpending(&pending);//获取该进程的pending表
//2.2打印pending表!
show_pending(pending);
sleep(1);
}
return 0;
}
==我们发现当我们使用ctrl+c发送2号信号后,2号信号就被阻塞!然后pending表里面的2更好信号位置就变了1!==
sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
和signal一样这也是一个信号捕抓函数!功能也是一样的!即对特定信号设定特定的回调方法!——但是sigaction有更详细的选项设置
==首先我们就要认识一下struct sigaction这个结构体==
//The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);//设置普通信号的回调函数
void (*sa_sigaction)(int, siginfo_t *, void *);//设置实时信号的回调函数
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
上面我们说过,信号处理的时候是不能去屏蔽同信号(因为会被自动的接触!)那么如果我们想要在处理本信号的时候,顺便屏蔽其他类型的信号呢?——可以的,我们可以添加进==sa_mask==里面,这样就是这个成员变量的作用!
#include<signal.h>
#include<iostream>
#include<unistd.h>
#include<cstdio>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%d",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
cout << "get a signo :" << signo << "is running"<< endl;
Count(20);
}
int main()
{
struct sigaction act,oact;
act.sa_flags = 0;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
//我们直接向
sigaddset(&act.sa_mask,3);//将三号信号设置进mask里面!
//这样子在除了屏蔽2号信号本身,还会将3号信号给屏蔽!
sigaction(SIGINT,&act,&oact);
while(true)
{
sleep(1);
}
return 0;
}
在处理2号信号的时候,2,3号信号就都被屏蔽了!
==只有处理完2号信号,操作系统才会将2号信号和3号信号都解除屏蔽!——此时3号信号就可以抵达了!这样子进程就被终止了!==
总结
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字
可重入函数
假设有一个链表,我们要对其进行头插!
正常看起来是这样没错!
==但是假如出现了一些情况!——插入的第一步的时候因为某些原因触发了信号!而信号handler是我们的自定义行为,里面一个也有插入会发生什么呢?==
==我们发现,虽然都执行成功了!——但是插入顺序却出现问题了!导致了我们内存节点丢失!==
之所以会出现这个问题,是因为==main执行流和信号捕抓执行流,两套不同的执行流,重复进入了同一个函数!==导致了代码结果出现了未定义或者出错的情况!——我们将这种函数称之为==不可重入函数!==
volatile
这是一个C语言的的关键字!——不过我们一般很少用!
==这个关键字的作用是保持内存的可见性!——这个说法听起来很抽象!==
我们下面的例子来说明这个
#include <stdio.h>
#include<signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d 号信号,正在被捕抓\n",signo);
printf("quit : %d",quit);
quit = 1;
printf("-> %d\n", quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("注意!该进程是正常退出的!\n");
return 0;
}
代码运行的结果不出我们所料!
SIGCHLD
这个信号是和进程等待有关系
如果一个子进程退出了!——那么这个子进程就会变成僵尸状态,然后让父进程读取它,获取子进程的退出码和退出信号!
如果一个子进程没有退出——那么父进程就要阻塞或者非阻塞的等待子进程!
==当子进程退出的时候,它不会直接就退出,然后进入僵尸状态,而是它在进入僵尸状态的时候!它会告诉父进程它的状态!==
==那么它是怎么告诉父进程的呢?——通过发送信号的方式!发送SIGCHLD信号!(17号信号)==
我们可以看到SIGCHLD的行为是Ign(ignore the signal)忽略该信号!这个忽略是内核级别的忽略
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%2d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
printf("pid: %d , %d 号信号,正在被捕抓\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);
printf("我是父进程!pid : %d,ppid: %d\n", getpid(), getppid());
pid_t id = fork();
while(id == 0)
{
printf("我是子进程!我要退出了!pid : %d,ppid: %d\n",getpid(),getppid());
Count(5);
exit(1);
}
while(1) sleep(1);
return 0;
}
==用上面断点我们可以看出来!——子进程退出之后!父进程确实会捕抓子进程发出的信号!==
那么如果知道这一点的话有什么用处呢?
如果不知道这一点,我们想要知道子进程退出什么时候退出!我们只能主动的去调用waitpid和wait这样的函数!——无论是阻塞等待还是非阻塞等待
==但是现在我们知道了!我们其实可以不用去主动关心子进程!等子进程退出了!那么它就会自己给父进程发送信号!这时候父进程再去回收子进程!==
void handler(int signo)
{
//我们是可以在handler里面直接调用wait或者waitpid的!
}
但是这样子代码的健壮性不好!
==情况1:假如我们有很多的子进程!(例如:10个)在同一时刻同时退出==
那么就会向父进程同时发送10个SIGCHLD信号!==但是上面我们讲过当正在处理一个信号的时候,操作系统会自动屏蔽同类型的信号!而且因为pending表只有一份!后续的9个信号也只能被保存一份!==——所以在handler里面直接调用wait或者waitpid是不好的!
所以最好的方式是通过循环等待!——因为我们并不知道有几个进程退出了!所以只能while(1) --> waipid式的等待