国外 网站网站,郑州大型网站公司,国内知名软件开发公司,设计师培训大概多少钱目录
一、sigaction()
使用
信号捕捉技巧 二、可重入函数
三、volatile关键字
四、SIGCHLD信号 在信号这一篇中我们已经学习到了一种信号捕捉的调用接口#xff1a;signal(),为了深入理解操作系统内核中的信号捕获机制#xff0c;我们今天再来看一个接口#xff1a;si…目录
一、sigaction()
使用
信号捕捉技巧 二、可重入函数
三、volatile关键字
四、SIGCHLD信号 在信号这一篇中我们已经学习到了一种信号捕捉的调用接口signal(),为了深入理解操作系统内核中的信号捕获机制我们今天再来看一个接口sigaction()
一、sigaction() sigaction 一个系统调用用于修改和/或查询信号的处理方式。它提供了对信号处理函数更精细的控制相较于旧的 signal 函数来说更为强大和灵活。
1、int signum:该参数需要传入指定的进程信号表示要捕捉的信号
2、const struct sigaction *act:该参数与函数同名是一个结构体指针如图 1、void (*sa_handler)(int);,此成员的含义其实就是 自定义处理信号的函数指针; 2、void (*sa_sigcation)(int, siginfo_t *, void *);此成员也是一个函数指针. 但是这个函数的意义是用来 处理实时信号的, 不考虑分析. (siginfo_t 是表示实时信号的结构体) 3、sigset_t sa_mask;, 此成员是一个信号集, 这个信号集有什么用呢我们在使用时解释 4、int sa_flags;, 此成员包含着系统提供的一些选项, 本篇文章使用中都设置为0 5、void (*sa_restorer)(void);, 很明显 此成员也是一个函数指针. 但我们暂时不考虑他的意义. 也就是说我们暂时将该接口的第二个参数简单理解为一个结构体指针并且结构体里有一个成员是用来自定义处理信号的。即该参数的作用就是将指定信号的处理动作改为传入的struct sigaction的内容。 3、struct sigaction *oldact,第三个参数类似sigprocmask()接口中的第三个参数一样都是输出型参数且其作用是获取 此次修改信号 struct sigaction之前的原本的 struct sigaction ,如果传入的是空指针则不获取。
使用
我们实现一个捕捉2号信号的程序然后让他按照我们自定义的处理动作执行且将自定义的处理动作设置为死循环也就是让进程在收到2号信号后一直执行该信号的处理动作。
#includeiostream
#includeunistd.h
#includesignal.h
using std::cout;
using std::endl;void handler(int signum)
{coutI catch a signal::signumendl;sigset_t pending;while(true){sigpending(pending);int sig1;for(sig1;sig31;sig){//利用死循环打印未决信号集if(sigismember(pending,sig)){cout1;}else{cout0;}}coutendl;sleep(1);}
}
int main()
{struct sigaction act,oact;//实例化出两个结构体对象作为参数act.sa_handlerhandler;//初始化自定义处理动作act.sa_flags0;//先设置为0sigemptyset(act.sa_mask);//初始化sigaction(2,act,oact);//捕捉2号信号while(true){coutI am a process!My pid::getpid()endl;sleep(1);}return 0;
}可以看到在我们第一次发送了2号信号后其打印出来的未决信号集全是0这是因为在没有捕捉到2号信号前该进程的未决信号集全为0捕捉之后第二个位置应该为1然后开始处理自定义动作前将1又置为0表示开始处理自定义动作此时就一直处于死循环中即一直在执行自定义动作。 当我们第二次发送2号信号时它的未决信号集的对应位置就变为了1后面再发送2号信号时该信号都会被拦截下来导致后面的2号信号一直处于未决状态。但是发送其他信号进程又会处理其他信号。
那么我们要是想要在处理2号信号的同时还要将其他信号拦截呢这时候就与 sa_mask 相关了顾名思义了就是信号屏蔽字。 struct sigaction结构体的sa_mask 成员的意义是, 添加进程在处理捕捉到的信号时对其他信号的阻塞. 如果需要添加对其他信号的阻塞, 那么就可以继续在 sa_mask 中添加其他信号. 不过, 这样做有什么意义呢 这样做可以 防止用户自定义处理信号时, 嵌套式的发送其他信号并捕捉处理. 如果 用户的自定义处理信号方法内部, 还会发送其他信号, 并且用户还对其进行了捕捉. 那么 信号的处理就无止尽了. 这种情况是不允许发生的. 所以 可以通过使用 sa_mask 来进行对其他信号的拦截阻塞. 信号捕捉技巧
为了避免捕捉不同的信号并做处理时编写不同的处理函数太过于麻烦我们可以考虑通过传入相同的函数指针实现对不同信号的不同处理。 当我们定义完指定信号的处理函数之后, 我们可以再定义一个 handlerAll(int signo) 函数, 并使用 switch 语句, 将不同的 signo 分别处理.
此时, 我们在使用 signal() 或者 sigaction() 捕捉信号时, 就只需要统一传入 handlerAll 的函数指针就可以了.
这是一种 解耦技巧 二、可重入函数
可重入函数Reentrant Function是指可以在多线程环境中安全使用的函数即这个函数可以被多个线程同时调用而不会导致数据错误或不一致。
下面用个例子解释一下
一个进程中, 存在一个全局的单链表结构. 并且此时需要执行一个节点的头插操作 此时需要让该结点的指向下一个节点的指针指向头节点再将自己变为头节点
node1-next head;
head node1;如果在刚执行完第一步之后, 进程因为硬件中断或者其他原因 陷入内核了.
陷入内核之后需要回到用户态继续执行, 切换回用户态时 进程会检测未决信号, 如果此时刚好存在一个信号未决, 且此信号自定义处理.并且, 自定义处理函数中 也存在一个新节点头插操作: 此时又会执行node2节点的头插 执行完后node2节点暂时就成为了新的头节点 接着进程返回用户态去执行剩下的代码即 headnode1 , 导致的结果就是node2节点最终找不到了这样就造成了 内存泄漏 是因为 单链表的头插函数, 被不同的控制流程调用了, 并且是在第一次调用还没返回时就再次进入该函数, 这个行为称为 重入 而 像例子中这个单链表头插函数, 访问的是一个全局链表, 所以可能因为重入而造成数据错乱, 这样的函数 被称为 不可重入函数, 即此函数不能重入, 重入可能会发生错误 反之, 如果一个函数即使重入, 也不会发生任何错误(一般之访问函数自己的局部变量、参数), 这样的函数就可以被称为 可重入函数. 因为每个函数自己的局部变量是独立于此次函数调用的, 再多少次调用相同的函数, 也不会对之前调用函数的局部变量有什么影响. 如果一个函数符合以下条件之一则称为不可重入函数 调用了malloc和free因为 malloc 也是用全局链表来管理堆的。调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构 三、volatile关键字 在之前学习C/C的时候我们就已经接触过这个关键字了它的作用是防止编译器对该关键字修饰的变量的优化确保每次访问这个变量时都直接从内存中读取而不是使用可能存在的寄存器中的缓存值。这是因为在某些情况下变量的值可能会被外部因素改变如硬件中断、多线程环境下的其他线程等。
接下来我们用一个例子分析一下该关键字
下面的程序是先定义一个全局变量flag以该全局变量作为触发条件当为0时一直处于死循环状态。当为1时程序正常结束。在main()中不对flag做修改只有在捕获到2号信号的时候在自定义的处理函数中才会对flag做出修改。
#include stdio.h
#include signal.hint flags 0;void handler(int signo) {printf(获取到一个信号,信号的编号是: %d\n, signo);flags 1;printf(我修改了flags: 0-1\n);
}int main() {signal(2, handler);while (!flags);// 未接收到信号时, flags 为 0, !flags 恒为真, 所以会死循环printf(此进程正常退出\n);return 0;
}可以看到在发送了2号信号后程序正常结束。 虽然 2信号的自定义处理函数 会对flags作出修改, 但是这个函数的执行是未知的. 即 不确定进程是否会收到2信号 进而执行此函数.那么对编译器来说, 就有可能对 flags 做出优化.我们知道, 进程再判断数据时, 是CPU在访问数据, 而CPU访问数据时 会将数据从内存中拿到寄存器中. 然后再根据寄存器中的数据进行处理.在此例中, 就是 while(!flags); 判断时, CPU会从内存中拿出数据进行判断. 当flags从0变为1时, 是内存中的数据发生了变化, CPU也会从内存中拿到新的数据进行判断而 此例中编译器可以确定一定会执行的代码中, flags是不会被修改的. 那么 编译器就可能针对flags做出优化由于编译器认为进程不会修改 flags, 那么在 while(!flags); 判断时, CPU读取到flags为0 并存放在寄存器中之后, 为了节省资源 在之后的判断中 CPU 不会再从内存中读取数据, 而是直接根据寄存器中存储的数据进行判断. 这就会造成 即使处理信号时将 flags 改为了1, 在进行 while(!flags);判断时, CPU依旧会只根据寄存器中存储的0 来进程判断, 这就会造成 进程不会正常退出 我们可以在编译是使用 -02 选项让编译器做出这样的优化 可以看到即使是flag改成了1程序依然不会停止
接着我们使用关键字修饰这个变量 volatile int flag0 再查看结果 可以看到已经没有优化了。
四、SIGCHLD信号
我们之前讲到过在子进程退出的时候是需要让父进程接收退出信息的否则子进程会进入僵尸状态所以我们介绍了有关于进程等待的函数让父进程主动去询问子进程是否退出事实上子进程退出的时候是会通知父进程的只不过父进程会忽略而已。
在子进程退出的时候子进程会向父进程发送一个信号即 SIGCHID
下面我们在父进程中捕捉这个信号看看情况
#includeiostream
#includecstdlib
#includesignal.h
#includeunistd.h
using std::cout;
using std::endl;
using std::cerr;
void handler(int signum)
{coutChild process has exited,mypid::getpid()Signal num::signumendl;
}
int main()
{signal(SIGCHLD,handler);pid_t idfork();if(id0){cerrfork error!endl;exit(1);}else if(id0){while(true){coutI am child process,mypid::getpid()endl;sleep(2);}exit(0);}while(true){coutI am parent process!my pid::getpid()endl;sleep(2);}return 0;}17号信号即 SIGCHID信号 且默认动作是Ign(忽略) 但是对于该信号内容中说明 子进程暂停或终止即在子进程暂停或终止的时候都会发送该信号给父进程。我们做个测试首先需要知道暂停信号是19号继续信号是18号信号 可以看到无论是暂停、继续、还是终止子进程的时候其都会向父进程发送该信号。
那么我们知道了这个信号又有什么用处呢 在介绍进程等待时 提到过waitpid()接口会等待子进程退出, 而等待的动作是主动去询问子进程是否退出. 现在我们清楚了子进程在退出的时候会发送SIGCHID信号给父进程那我们让父进程可以通过捕捉该信号去等待子进程。
#includeiostream
#includecassert
#includeunistd.h
#includesys/types.h
#includesys/wait.h
#includestdlib.h
#includesignal.h
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signoSIGCHLD);pid_t idwaitpid(-1,nullptr,0);if(id0){coutWaiting success!pid::getpid()child process id::idendl;}}int main()
{signal(SIGCHLD,Childprofree);pid_t idfork();if(id0){coutPerror fork!endl;exit(0);}else if(id0){while(true){coutI am child process!my pid::getpid()endl;sleep(2);}exit(0);}while(true){coutI am parent process!my pid::getpid()endl;sleep(2);}return 0;
}上面的程序只针对单进程的情况如果是多个进程的情况下就会有一些问题 我们利用循环创建10个子进程这10个子进程在打印完十次之后自动退出。
#includeiostream
#includecassert
#includeunistd.h
#includesys/types.h
#includesys/wait.h
#includestdlib.h
#includesignal.h
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signoSIGCHLD);pid_t idwaitpid(-1,nullptr,0);if(id0){coutWaiting success!pid::getpid()child process id::idendl;}}int main()
{signal(SIGCHLD,Childprofree);for(int i0;i10;i){pid_t idfork();if(id0){coutPerror fork!endl;exit(0);}else if(id0){int cnt10;while(cnt){coutI am child process!my pid::getpid()cnt::cnt--endl;sleep(2);}coutChild process being Z!endl;exit(0);}}while(true){coutI am parent process!my pid::getpid()endl;sleep(2);}return 0;
}我们在右边窗口使用 while :;do ps ajx|head -1ps ajx|grep mydwait|grep -v grep;sleep 2;done命令循环查看进程情况
运行结果如图 可以看到等到子进程推出的时候按道理是所有的子进程都退出都被父进程回收掉但是只有左边红色框里这几个进程退出了右边红色框里还有几个没有被回收掉一直处于僵尸状态。 在Linux中每个进程都有一个信号集用来表示该进程当前接收到的、尚未处理的信号。这个集合有一个重要特点它是基于信号类型的而不是基于信号的数量。这意味着对于同一类型的信号例如多个 SIGCHLD操作系统不会为每个信号单独排队而是只会记录该类型信号至少发生过一次。换句话说如果多个 SIGCHLD 信号几乎同时到达操作系统会将它们合并成一个信号并只传递给父进程一次。 因此如果大量的子进程几乎在同一时间结束父进程可能只接收到一个 SIGCHLD 信号而实际上有多个子进程已经终止。这就导致了父进程可能没有机会处理所有终止的子进程从而留下僵尸进程。 那么怎么修改处理信号的方式呢我们将回收子进程的部分设置为 利用死循环回收在没有子进程的情况下跳出循环。
一旦有收到子进程的退出信号后这个外部的函数就会进入死循环不断等待释放需要退出的子进程直到没有子进程需要释放了就退出。事实上这个改动就跟收到信号没有关系了单纯利用循环不断等待需要被释放的子进程。 void freeChild(int signo) {assert(signo SIGCHLD);while(true) {pid_t id waitpid(-1, nullptr, 0);if (id 0) {cout 父进程等待子进程成功, child pid: id endl;}else {cout 等待结束 endl;break;}}
}可以看到最后所有的进程都退出了。 但是新的问题又出现了一旦有子进程不退出的话父进程就不会再运行了因为我们设置的waitpid()的第三个参数为0是阻塞式等待所以会一直处在外部的自定义处理函数中不会回到main()函数
观察下面的情况我们让一部分子进程循环5次后退出一部分循环30次后退出
#include cassert
#include cstdlib
#include iostream
#include signal.h
#include sys/types.h
#include sys/wait.h
#include unistd.h
using std::cout;
using std::endl;void freeChild(int signo) {assert(signo SIGCHLD);while (true) {pid_t id waitpid(-1, nullptr, 0);if (id 0) {cout 父进程等待子进程成功, child pid: id endl;}else {cout 等待结束 endl;break;}}
}int main() {signal(SIGCHLD, freeChild);for (int i 0; i 10; i) {pid_t id fork();if (id 0) {// 子进程int cnt 0;if(i 6)cnt 5; // 前6个子进程 5就退出elsecnt 30; // 后4个子进程 30退出while (cnt) {cout 我是子进程, pid: getpid() , cnt: cnt-- endl;sleep(1);}cout 子进程退出, 进入僵尸状态 endl;exit(0);}}// 父进程while (true) {cout 我是父进程, pid: getpid() endl;sleep(1);}return 0;
}可以看到在所有进程退出之前父进程代码并没有运行我们只需要将 waitpid()的第三个参数改为 WNOHANG 就可以了表示 非阻塞式等待。 下面是最终版代码 #includeiostream
#includecassert
#includeunistd.h
#includesys/types.h
#includesys/wait.h
#includestdlib.h
#includesignal.h
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signoSIGCHLD);while(true){pid_t idwaitpid(-1,nullptr,WNOHANG);if(id0){coutWaiting success!pid::getpid()child process id::idendl;}else{coutWating end!endl;break;}}}int main()
{signal(SIGCHLD,Childprofree);for(int i0;i10;i){pid_t idfork();if(id0){coutPerror fork!endl;exit(0);}else if(id0){int cnt 0;if(i 6)cnt 5; elsecnt 30; while(cnt){coutI am child process!my pid::getpid()cnt::cnt--endl;sleep(2);}coutChild process being Z!endl;exit(0);}}while(true){coutI am parent process!my pid::getpid()endl;sleep(2);}return 0;
}