文章目录
- 一、信号的捕捉
- 1.用户态和内核态
- 2.内核如何实现信号的捕捉
- 3.sigaction
- 二、可重入函数
- 三、volatile
- 四、SIGCHLD信号
一、信号的捕捉
1.用户态和内核态
用户态的的时候,进行以下操作:1.操作系统自身的资源(getpid,waitpid…)2.硬件资源(printf, write,read)
用户为了访问内核或者硬件资源,必须通过系统调用完成访问。实际执行系统调用”人是“进程”,但是身份其实是内核。往往系统调用比较费时间一些,所以尽量避免频繁调用系统调用
CPU中有两类寄存器:1.可见寄存器2.不可见寄存器。凡是和当前进程强相关的,上下文数据都保存在寄存器中。CR3寄存器表征当前进程的运行级别;0:内核态,3表示用户态
我一直不太理解:我是一个进程,怎么跑到OS中执行方法呢?
每个进程都有自己独立的用户级页表,内核级页表只有一份就够了
每一个进程都有自己的地址空间(用户空间独占)内核空间(被映射到了每一个进程的34G)。进程要访问OS的接口,其实]只需要在自己的地址空间上进行跳转就可以了!!每一个进程都有34GB,都会共享一个内核级页表,无论进程如何切换,会不会更改任何的[3.4]。用户,凭什么能够执行访问内核的接口或者数据呢?系统调用接口,起始的位置会帮你做的!Int 80 --陷入内核
2.内核如何实现信号的捕捉
信号产生的时候,不会被立即处理,而是在合适的时候。从内核态返回用户态的时候,进行处理,说明曾经我一定是先进入了内核态!----系统调用,进程切换
线程通过系统调用陷入内核,完成了从用户态到内核态的转变你,然后遍历block和pending表,以及映射的hander,对信号进行默认/忽略/自定义的捕捉,对于自定义捕捉,操作系统通过特定的调用,将自己的身份重新改为用户态,执行自定义函数,执行完毕之后,又通过特殊的系统调用sigreturn再次回到内核,继续进行信号检测,然后返回用户模式,从上次被中断的地方继续向下执行。
3.sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动
作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回
值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信
号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <signal.h>
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r", cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
std::cout << "get a signo: " << signo << "正在处理中..." << std::endl;
Count(20);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigemptyset(&act.sa_mask);
// sigaddset(&act.sa_mask, 3);
sigaction(SIGINT, &act, &oact);
while (true)
sleep(1);
return 0;
}
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数
二、可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
三、volatile
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
#include <stdio.h>
#include <unistd.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;
}
不加volatile就会一直休眠
加了之后,收到2号信号后直接退出
四、SIGCHLD信号
wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
void hanlder(int signo)
{
pid_t id;
int status;
if (id = waitpid(-1, &status, 0) > 0)
{
std::cout << "wait child process success,lastcode: " << ((status >> 8) & 0xff) << std::endl;
}
std::cout << "child is quit" << std::endl;
}
int main()
{
signal(SIGCHLD, hanlder);
pid_t id = fork();
if (id == 0)
{
std::cout << "child create seccuess" << std::endl;
sleep(3);
exit(2);
}
while (true)
{
std::cout << "father process is doing other things" << std::endl;
sleep(1);
}
return 0;
}
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。
请编写程序验证这样做不会产生僵尸进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
signal(SIGCHLD, SIG_IGN);
pid_t id = fork();
if (id == 0)
{
std::cout << "child create success" << std::endl;
sleep(3);
exit(2);
}
int status = 0;
id = waitpid(id, &status, 0);
if (id > 0)
{
std::cout << "wait child process success,lastcode: " << ((status >> 8) & 0xff) << std::endl;
}
std::cout << "child is quit" << std::endl;
return 0;
}