linux之信号操作
sigset_t
这是信号在内核中的表示
==block和pending都是位图——即用bit位来表示信号编号!==
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用==相同的数据类型sigset_t==来存储,==sigset_t称为信号集==,这个类型可以表示每个信号 的“有效”或“无效”状态(在软件层面上就是将bit为从0置为1,或者从1置为0,硬件上就是从充放电来表示)
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
同一种位图我们可以用不同的解释来改变它的含义
==阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),==这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释(如用printf直接打印sigset_t变量是没有意义的)
虽然本质都是位图,但是每个操作系统的实现方式是不一样的!不要自己使用逻辑与或非去进行判断,操作
#include <signal.h> int sigemptyset (sigset_t *set); int sigfillset (sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset (sigset_t *set, int signo); int sigismember (const sigset_t *set, int signo);
==参数set就是——信号集,参数signo——就是那个信号(那个比特位)==
函数sigemptyset——初始化set所指向的信号集,使其中所有信号的对应bit都置为0。表示该信号集不包含任何有效信号。
函数sigfillset——初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
函数sigaddset——将一个特定的信号添加进这个信号集里面!
函数sigdelset——将特定的信号从信号集里面删除
函数sigismember——判断该信号存不存在信号集里面
前4个函数都是成功返回0,出错返回-1。
==sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。==
sigprocmask
==这个函数的作用就是用来更该进程的block表!==——那个进程调用就修改那个进程!
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
==how参数==——表示如何修改这个block表,有三个不同的选项,SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK
BIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号(新增信号屏蔽字),相当于mask=mask|set BIG_UNBLOCK st包含了我们希望从当前信号屏蔽字中解除阻塞的信号(删除信号屏蔽字),相当于mask=mask&~set SIG_SETMASK 设置当前信号屏蔽字为set所指向的值(重置当前的信号屏蔽字重置为set),相当于mask=set 如何理解三个选项呢?——就像有个老师布置作业,说布置800字的作文
老师在在800字作文的基础上,继续增加作业,例如多三道数学题——这就是SIG_BLOCK(增加)
老师将800字作文和三道数学题,变成800字作文和一道数学题——这既是BIG_UNBLOCK(减少)
老师收到通知将800字作文和三道数学题,变成了一个物理实验报告!重新布置作业,其他就不用做了——这就是SIG_SETMASK(即重置)
==set参数==——这个参数和how参数强相关!
假如我们想要对该进程的所有信号进程屏蔽!
那么我们就可以传入一个全1的set信号集!——使用sigfilset函数设置!
然后选择SIG_SETMASK操作(重置操作),就会将我们传进的set的位图结构,设置进进程的block位图里面!
==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!==
9号信号是绝不能被屏蔽,绝不能被捕抓!
==如果我们想要接触信号屏蔽!==
#include<iostream> #include <signal.h> #include<string> #include<vector> #include<unistd.h> #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() { sigset_t set,oset,pending; sigemptyset(&set);//全部设为0 sigemptyset(&oset); sigemptyset(&pending); for(auto sig:sigv) { sigaddset(&set, sig); } sigprocmask(SIG_BLOCK,&set,&oset); int cnt = 5; while(true) { //2.1获取 sigpending(&pending);//获取该进程的pending表 //2.2打印pending表! show_pending(pending); sleep(1); //cnt秒之后接触屏蔽! if(cnt-- == 0) { std::cout << "Restore all signals" << std::endl; sigprocmask(SIG_SETMASK,&oset,&set); //sigprocmask(SIG_UNBLOCK,&set,&oset);//这样子也可以! //这里无法打印! std::cout << "Restore all signals" << std::endl; } } return 0; }
我们会发现5s之后信号确实被解除了阻塞,直接让进程被终止了!但是为什么没有打印两个语句出来呢??
==一旦对特定信号接触屏蔽,那么操作系统至少要抵达一个信号!==
那么从内核态返回用户态之后就把进程终止了,压根不会回到进程的里面继续执行后续代码,所以无法打印!!!
==如果想要信号在被接触阻塞之后能打印!——我们可以对信号进行捕抓!==
#include<iostream> #include <signal.h> #include<string> #include<vector> #include<unistd.h> #define MAX_SIGNUM 31 static void myhandler(int signo) { std::cout << "The signal has been arrested" << std::endl; std::cout << "Restore all signals" << std::endl; } int main() { for(auto sig:sigv)//信号捕抓! { signal(sig,myhandler); } //.... return 0; }
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_handler这个成员变量!,sa_sigaction一般设置为nullptr,sa_flags也设置为0即可,sa_restorer也是和实时信号相关(一般也不使用)——设置为nullptr即可==(一般sa_handler和sasa_sigaction是最好不要同时同时使用!)
==我们后面主要讨论的是sa_mask这个成员变量!==
参数signum——就是信号编号!
参数act——是一个输入信参数,对我们当前进程的信号捕抓逻辑进行影响或者修改!
信号oldact——是一个输出型参数!获取特定信号老的处理方法!
返回值——成功返回0,不成功返回-1
==我们可以试用一下==
#include<signal.h> #include<iostream> #include<unistd.h> using namespace std; void handler(int signo) { cout << "get a signo :" << signo << endl; } int main() { struct sigaction act,oact; act.sa_flags = 0; act.sa_handler = handler; sigemptyset(&act.sa_mask); sigaction(SIGINT,&act,&oact); while(true) { sleep(1); } return 0; }
==我们可以看到和我们使用signal函数其实是一样的!那么sigaction函数和signal函数的区别究竟是什么?==
#include<signal.h> #include<iostream> #include<unistd.h> #include<cstdio> using namespace std; void Count(int cnt) { while(cnt) { printf("cnt: %d\r",cnt); fflush(stdout); cnt--; sleep(1); } printf("\n") ; } void handler(int signo) { cout << "get a signo :" << signo << "is running"<< endl; Count(20);//我们使用Count函数来模拟handler处理时间长的场景! } int main() { struct sigaction act,oact; act.sa_flags = 0; act.sa_handler = handler; sigemptyset(&act.sa_mask); sigaction(SIGINT,&act,&oact); while(true) { sleep(1); } return 0; }
**当handler函数==比较废时间的时==候,如果多次收到相同的信号会发生什么呢?**会不会在handler函数的内部递归式的调用handler呢?——也就是说进程在运行的时候可能会收到大量的同类型的信号!如果收到同类型的信号,当前又正在处理某一个信号时,会发生什么呢?操作系统会不会允许我们进行那么多的信号提交呢?
==我们可以发现在第一次收到信号信后,在信号处理期间,信号都不会再抵达!后面无论是收到了多少个信号都只会处理一个信号!==
当我们正在抵达某一个信号的期间!同类型信号无法被抵达!——因为当前信号被捕抓,==系统会自动将当前信号加入到信号的信号屏蔽字(block表里面!)==!而当信号完成捕抓动作!系统又会==自动解除该信号的屏蔽!==
如果我们该handler里面尝试的屏蔽正在抵达的信号!例如:2号信号!
==那么就会出现虽然我们屏蔽了!——但是屏蔽完过后又被操作系统给自动的解除了!==
还能捕抓一次是因为,在首次收到信号的时候,pending位图会被由1置0,然后后续收到信号的时候,就会再次将0置为1!——但是因为只有一个位图所以只能改一次!
==一般一个信号被解除屏蔽的时候,会自动进行抵达!(至少要抵达一次!)如果这个信号已经在pending表里面了!没有就不进行任何动作!==
==处理信号信号的原则是串行的处理同类信号!不允许递归的进行处理!(只有处理完一个,才能处理下一个!)==
上面我们说过,信号处理的时候是不能去屏蔽同信号(因为会被自动的接触!)那么如果我们想要在处理本信号的时候,顺便屏蔽其他类型的信号呢?——可以的,我们可以添加进==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执行流和信号捕抓执行流,两套不同的执行流,重复进入了同一个函数!==导致了代码结果出现了未定义或者出错的情况!——我们将这种函数称之为==不可重入函数!==
一般我们认为,main执行流和信号捕抓执行流是两套不同的执行流!——虽然在代码里面我们来看也是串行了!main想要后续继续执行,也依旧要等待信号捕抓执行流执行完!但是我们可以看到main执行流其实压根没有直接调用果handler函数!是通过信号过来后才回调过去的!所以我们才说是两套执行流!
如果在main中或者handler中该函数被重复进入后==出现了问题——那就是不可重入函数!(不能重复进入)==
==如果重复进入后没有出现问题!——那么就是可重入函数!==
我们平时用的大部分接口都是不可重入函数!——函数可不可被重入是一个特性!不是一个问题!我们不需要解决
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的
- 调用了标准I/O库函数。标准标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
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; }
代码运行的结果不出我们所料!
这里我们要说一点就是关于编译器的优化!当我们编译代码的时候,编译器都会进行优化,一般是O1或者O2,编译器的优化都是取决于编译器
我们可以自己手动的将优化级别调高
==当我们手动的提升编译器的优化级别!==
为什么会出现这个现象呢?
数据保存的无法就两个地方——一个是内存,一个是寄存器!那么优化其实也就是将数据从内存优化到寄存器里面!(因为寄存器比内存更快!)
所以这就是为什么明明quit被改了!但是循环却依旧不进行终止的原因!==因为寄存器中quit的存在,遮蔽了物理内存中quit变量存在的事实!==
在while循环只看到了寄存器!而看不到内存!
这种情况就会导致,我们代码没有问题!但是因为编译器的优化从而让代码没有按照预期来进行工作!
==为了解决这个问题!我们就要用到volatile关键字!==
在C/C++的里面,它的作用官方说法就是保持内存可见性!——==用简单的说法就是,让这个数据不要优化到寄存器里面!而是一直从内存里面进行读取!(这就是所谓的保持内存可见性)==
#include <stdio.h> #include<signal.h> volatile 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; }
我们依旧进行O3级别的优化!但是此时quit就不会被放进寄存器里面了!代码逻辑也就正常了!
如果出现了信号捕抓执行流和main函数执行流里面有个要修改的值的时候!这时候我们就要注意了!
SIGCHLD
标签:九千,int,进程,屏蔽,handler,信号,linux,字长,include From: https://blog.51cto.com/u_15835985/9195041这个信号是和进程等待有关系
如果一个子进程退出了!——那么这个子进程就会变成僵尸状态,然后让父进程读取它,获取子进程的退出码和退出信号!
如果一个子进程没有退出——那么父进程就要阻塞或者非阻塞的等待子进程!
==当子进程退出的时候,它不会直接就退出,然后进入僵尸状态,而是它在进入僵尸状态的时候!它会告诉父进程它的状态!==
==那么它是怎么告诉父进程的呢?——通过发送信号的方式!发送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式的等待
waitpid的第一个参数,我们一般都是设置为指定进程的pid!但是也可以设置为-1!即等待任意子进程的进程!
那么这个死循环什么终止呢?——waitpid再也等待不到的时候!说明底层的子进程都全部退出完毕了!
==情况2:我们有很多的子进程(10个),只有一部分退出了!==
这种情况下我们也是进行循环式的等待,假如有5个退出了,当waitpid已经回收了这5个的时候!第六个要不要进程waitpid呢?——==要的!因为进程压根不知道!到底退出了多少个子进程!我们说5个退出了是我们站在上帝视角来看待这件事情的!进程是不知道到底退出了多少个!所以即使将5个子进程都回收!也还是要进行waitpid!==
如果此时是阻塞式等待!那么就出现问题了!——在handler方法里面出现了阻塞式调用!那么就无法返回主进程了!——所以在循环式等待的时候==不能进行阻塞式等待!要进行非阻塞式等待!==——非阻塞式等待时,如果没有等待成功waitpid是会返回0的!
void handler(int signo) { pid_t id; while(1) { id = waitpid(-1,NULL,WNOHANG); if(id <= 0)//出错或者等待失败 break; } }
==这就是通过信号来完成进程等待!==
上面我们都是在说要么在main执行流要么在信号捕抓执行流里面调用wait'/waitpid来回收子进程,我们也可通过==不调用wait/waitpid==的方式来回收子进程!
要想不产生僵尸进程还有另外一种办法==:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN(忽略)==
这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的
但这是一个特例此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程
#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"); } int main() { signal(SIGCHLD,SIG_IGN);//显示的设置!对SIGCHLD进行忽略 printf("我是父进程!pid : %d,ppid: %d\n", getpid(), getppid()); pid_t id = fork(); while(id == 0) { printf("我是子进程!我要退出了!pid : %d,ppid: %d\n",getpid(),getppid()); Count(3); exit(1); } while(1) sleep(1); return 0; }
但是为什么要我们手动去显示将SIGCHLD设置为SIG_IGN?——我们上面看到过、
该信号的默认行为不就是IGN吗?——**默认的IGN和我们手动设置出来的是不一样的!**默认IGN行为就是我们看到的,如果没有被父进程回收那么就进入僵尸!
但是我们的自己设置的IGN就是不进入僵尸,也不等待,直接回收!
这两个值在操作系统内部肯定是不一样的!