【Linux】信号
目次生活中的信号
Linux中kill -l信号
信号概念的基本储备
信号的三个阶段
信号产生
信号保存
信号处理
内核态和用户态
可重入函数
volatile
SIGCHLD
生活中的信号
着实在我们中有各种各样的信号,比如闹钟响了,红灯停绿灯行等等,对于信号,我们有如下总结:
a.信号在生活中,随时可以产生,信号的产生和我是异步的
b.我们能认识这个信号
c.我们知道信号产生了,信号该怎么处理,也就是能识别并处理这个信号
d.我们大概做着更重要的事变,对到来的信号暂不处理,但是我记得这个事,在合适的时候去处理
把这里的 “我” 换成历程,就是我们今天要学习的信号。
Linux中kill -l信号
https://i-blog.csdnimg.cn/direct/8f03e49fb4cf4a70973f5df805d97f2a.png
着实,每个信号都是一个宏,前面的序号是宏对应的值。我们之前用过kill -9杀掉历程,假如向一个关闭读端的管道去写,历程会收到SIGPIPE信号,所以我们之前对信号有过一点粗浅的认识。
信号概念的基本储备
信号,Linux系统提供的一种,向指定历程发送特定事件的方式,系统会进行识别和处理。此外,信号是异步产生的。
对于信号处理,我们有三种方式:
1.默认动作
假如历程处理信号时,不做任何处理,那么都会接纳默认处理,这通常包括终止本身、停息、忽略。
https://i-blog.csdnimg.cn/direct/a8e14e3069804edabacfe5f754a8e1fc.png
2.忽略动作
3.自界说处理(信号的捕获)
当我不想执行信号的默认动作的时候,而是接纳我们所设计的方法,就可以接纳自界说处理,所接纳的函数调用是signal函数:
https://i-blog.csdnimg.cn/direct/054b0ab6373944c285905b603738a455.png
signal函数的第二个参数handler是函数指针,其返回值为void,参数类型是int,把函数名传递给signal的第二个参数,当历程收到signum之后,就会执行handler函数,handler函数的参数就是我们收到的信号,比如收到2号信号,那么sig就是2。我们写了如下代码来测试:
https://i-blog.csdnimg.cn/direct/530be9090d804a3792ac5617a9ad4d75.png
在这段代码中,一旦历程收到2号信号,就会去执行handler函数,我们看一下结果:
https://i-blog.csdnimg.cn/direct/7656db21194549658bfdd466cb413602.png
在上面的代码中,假如不停不产生2号信号会怎么样呢?handler方法不停不会被调用!也就是说只有收到了2号信号,才能执行handler方法。
此外,我们还可以对更多的信号进行捕获,我们再来看:
https://i-blog.csdnimg.cn/direct/80f2840aa984406ba27ea82d2e852183.png
https://i-blog.csdnimg.cn/direct/136d121f5eb44994a184bb57b6b38730.png
实验结果表明,我们给历程发几号信号,历程就会接受几号信号,并将信号值传给handler的sig参数。
那么,2号信号(SIGINT)到底是什么信号呢?是终止历程!
https://i-blog.csdnimg.cn/direct/002807875b614731a8262ea5de76db69.png
我们还发现,给历程发2号信号和给历程按下ctrl+c,都会让历程捕获到2号信号,都会执行handler方法。所以我们不难明确,我们之前说过假如非常历程,按ctrl+c键就可以终止历程,为什么能直接终止呢?因为ctrl+c就是给目标历程发送2号信号,而2号信号的默认动作就是终止历程。除了ctrl+c之外,ctrl+\ 也可以给历程发送信号终止历程(3号信号)。
那么,我们再来探讨一个标题:如何明白信号的发送与保存呢?
普通信号的数字是1~31,着实是用位图来保存收到的信号的!!!历程都有对应的task_struct,它是一个结构体,而结构体中会有许多成员变量,就可以用一个uint32_t signals这个变量来保存,它就是一个32个比特位(0000 0000 0000 0000 0000 0000 0000 0000,末了一个bit位不用),剩下恰好1~31个信号。之后,假如发送1号信号就将1位的bit位由0置1,就代表历程收到了1号信号,以此类推。所以,发送信号,着实是修改指定历程pcb中的信号的指定位图,0->1,与其说是发信号,不如说是写信号。pcb是内核数据结构,只有OS才有资格修改其值。
信号的三个阶段
接下来,我们围绕信号的三个阶段依次学习:信号产生、信号保存、信号处理。
https://i-blog.csdnimg.cn/direct/accd4ea9203748d08dff10121952cb59.png
信号产生
信号产生的方式有以下几种:
1.通过kill命令,向指定的历程发送指定的信号。
2.键盘可以给历程发信号。(比如ctrl+c)
我们还可以用系统调用kill,向指定历程发送指定信号,
https://i-blog.csdnimg.cn/direct/48d2efd7a3064a0794ae3731c1534fb6.png
使用kill系统调用,我们本身写一个mykill程序:
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
// ./mykill 2 1234
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv << " signum pid" << std::endl;
return 1;
}
pid_t pid = std::stoi(argv);
int signum = std::stoi(argv);
kill(pid, signum);
} https://i-blog.csdnimg.cn/direct/14e7059d94704eb4ac2cdd71133ccf0a.png
所以,我们的第三个产生信号的方式:
3.系统调用 int kill(pid_t pid , int sig) raise abort
除了kill之外,我们还可以使用raise系统调用来给历程发送信号:
https://i-blog.csdnimg.cn/direct/fde13f6e68684f88bd0e912c6a87f946.png
与kill所差异的是,谁调用raise,就给谁发送信号。也就是kill(getpid(),xxx) == int raise(int sig)。
我们还可以使用abort系统调用来终止历程,这着实是6号信号SIGABRT,
https://i-blog.csdnimg.cn/direct/16129d125b6b41e093bb471f4fc3205a.png
https://i-blog.csdnimg.cn/direct/7801449a06274810ae11db84aff81b4b.png
像abort这样的信号,虽然允许捕获,但是被捕获后仍然会被终止历程。
学到这里,我们大概有这样的标题:
a.假如我把全部信号都捕获了,会怎么样?
毕竟上,我们考虑到的,系统早就考虑到了,9号信号不允许捕获!
b.如何明白上面的信号发送?
虽然我们可以通过上面三种方式产生信号,但是真正发送信号的只能是OS!!!因为发送信号的本质着实是修改历程pcb中的位图,只有OS有资格去修改它本身界说申请的数据结构。
4.软件条件产生信号
我们之前学过,假如一个管道的读端直接关闭,而写端不停进行,此时OS会想写历程发送SIGPIPE(13号信号),这着实是软件条件。
这次我们要学习的是alarm系统调用,其作用是终止历程,
https://i-blog.csdnimg.cn/direct/294f2454d69f4253a8f282ff7b144315.png
闹钟是给未来几秒(seconds)之后设置的,这着实对应14号信号SIGALRM,假如seconds设置为5,那么就表示历程5s后收到SIGALRM信号。
https://i-blog.csdnimg.cn/direct/1c0e35ba00a141189a74ec87e8bf00e6.png
https://i-blog.csdnimg.cn/direct/01498abae78544509002e91551798cfc.png
1s后,我们程序中这个循环大概能执行20000多次,
接下来我们稍微改一下程序,不在每次循环中都打印输出,
https://i-blog.csdnimg.cn/direct/30d8c815510645bfbb3c06a415c40116.png
https://i-blog.csdnimg.cn/direct/3830460ed8de4d02bd7b54f840993647.png
我们发现,cnt的值已经加到了5亿多次,比上面的程序快了非常多,因为此时在循环里++的时候,是一个纯内存级的数据递增,而之前的每次需要打印在显示器上,中间经历了云服务器,需要跨网络,所以我们可以得出一个结论:IO很慢。
现实上,历程中会存在许多历程,所以OS要对闹钟做管理,照旧我们熟悉的先描述再构造,闹钟着实是一个结构体struct alarm:
struct alarm
{
time_t expired;//未来的超时时间=seconds+Now()
pid_t pid;
func_t f;
...
} 那如何对闹钟历程管理呢?着实,可以按未来的超时时间设置一个最小堆,堆顶就是下一个要执行的闹钟,对闹钟的管理就可以转换为对最小堆的增删查改。
那alarm系统调用的返回值是什么呢?假如alarm的参数为0(alarm(0)),它的意思是取消闹钟,返回值为上一个闹钟的剩余时间。
https://i-blog.csdnimg.cn/direct/55a87e6c6f22402cb9d7ed88b10b44d8.png
https://i-blog.csdnimg.cn/direct/ec73a530199a48c6a90ac8423dc5a97a.png
我们看下面的代码,闹钟设置一次,就默认只被触发一次:
https://i-blog.csdnimg.cn/direct/169d8f40fdc44457a9b5749512f8d745.png
https://i-blog.csdnimg.cn/direct/aa9df2fc23544198820771aae4dc4fc6.png
假如我们想不停触发闹钟该怎么办呢?现实上,可以在handler函数中,再设一个闹钟,这样每次调用handler后,都会重新设置闹钟。
https://i-blog.csdnimg.cn/direct/36f81555632a43e38abea58638391802.png
5.非常
我们之前或多或少大概都会遇到这样的非常环境(除0、野指针) :
https://i-blog.csdnimg.cn/direct/fce53e75676c4ab79d951e2a59544f85.png
遇到这样的非常,程序为什么会崩溃呢?这是因为非法访问,导致OS给历程发送信号啦!!详细来说,假如发生除0,是历程收到了8号信号SIGFPE,我们可以使用signal函数调用验证一下:
https://i-blog.csdnimg.cn/direct/56ace8b5769b43f89e89c0c78559de14.png
https://i-blog.csdnimg.cn/direct/7143291768e44f02b28f15cf6bf831b7.png
可见,当发生除0非常时,会给历程发生8号信号。
假如发生野指针非常,会给历程发送11号信号SIGSEGV,同样地,我们可以使用signal函数调用验证一下:
https://i-blog.csdnimg.cn/direct/dca3ecd2269b4fd9a19d3e33422e4af1.png
https://i-blog.csdnimg.cn/direct/a1b353f52ae6468fa48aeeff2a4fdc86.png
程序崩溃了为什么会退出?因为OS给历程发的信号默认是终止历程。可以不退出吗?可以,因为可以捕获非常,但是保举终止历程,为什么保举终止历程呢?下面我们深入探讨一下:
https://i-blog.csdnimg.cn/direct/cc2239d544864d509e81f5f9f24115d1.png
a变量存在于内存中,算数运算和逻辑运算在CPU中进行,而CPU中存在许多寄存器,包括ecp、ebp等这样的普通寄存器,还有状态寄存器eflag,CPU是可以做计算的,但是数据是来自于用户的,这注定了有些运算是精确的,有些是错误的,那CPU如何得知运算时正常的照旧非常的呢?现实上,状态寄存器中存在溢出标记位,当CPU在计算10/0时,除法运算会被转为加法运算,CPU中加法器不停做累加,累加到肯定程度时发生了数据溢出,溢出标记位由0置1,假如计算正常,CPU会把计算结果返回给内存,假如非常,溢出标记位被置1,这就表示运算堕落了。OS是软硬件资源的管理者,OS要随时处理这种硬件标题,也就是向目标历程发送信号。
还有一个标题,发生非常时OS给历程发一次信号不就可以了吗?为什么刚才会疯狂打印呢?我们知道,寄存器只有一套,但是寄存器内里的数据是属于每一个历程的,由于要进行历程切换,所以要有硬件上下文的保存和恢复。这个非常历程由于被捕获非常不退出,当重新切换到这个历程时,把之前保存的数据重新恢复到寄存器里,这就又给OS恢复了错误的数据,就会不停触发溢出标记位的错误。 像状态寄存器这种,历程无法访问到,自然无法更改其中的值。
末了,回到标题本身,为什么出非常后,保举终止历程呢?终止历程的本质是释放上下文数据,包括溢出标记数据大概其他非常数据!!历程出非常后,历程就没故意义了,就应该把历程的数据删掉了。
上面我们所说的是发生除0非常,那野指针非常也是一样的环境吗? CPU中有一个寄存器叫CR2,叫做页故障线性地址寄存器,当虚拟地址向物理地址转化失败,会把虚拟地址放在CR2里,这就表明CPU中出现了硬件错误,OS发现CR2中有了内容,OS就会向历程发送11号SIGSEGV信号,然后每次历程切换后,之前保存的数据重新恢复到寄存器里,OS就会不停给历程发信号。
https://i-blog.csdnimg.cn/direct/f260e52fa97e4afc94172944c6b114f4.png
现在我们再来讨论几个小标题,我们看到,历程信号有的事Core,有的是Term,它们有什么区别呢?着实,是Term的历程信号会使历程非常终止,是Core的历程信号会使历程非常终止,但是他会帮我们形成一个debug文件,这个debug文件是历程退出时的镜像数据(比如执行到哪里非常,非常的代码等核心数据),这个过程我们称为核心转储,存储到磁盘上。core是帮助我们进行debug的文件,便于事后调试。下图是我们在学习历程控制时的获取子历程status参数,其中的core dump标记假如为0,表示没有核心转储,为1,则表示有核心转储。
https://i-blog.csdnimg.cn/direct/e20161a016ef483a957ac90a442544b2.png
那么,我们来写代码看一下core dump标记位:
int Sum(int start, int end)
{
int sum = 0;
for(int i = start; i <= end; i++)
{
sum /= 0;
sum += i;
}
return sum;
}
int main()
{
// int total = Sum(1,100);
// std::cout<< "total: " << total << std::endl;
pid_t id = fork();
if(id == 0)
{
sleep(1);
//child
Sum(1,100);
exit(0);
}
//father
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
printf("exit code: %d,exit sig: %d,core dump: %d\n",(status>>8)&0xFF , status&0x7F,(status>>7)&1);
}
return 0;
} 在子历程调用Sum函数时,故意写了除0错误,我们运行以上程序,得到下面结果
https://i-blog.csdnimg.cn/direct/3b6d3519920e4ce3a200ea0f3bd64ead.png
发现core dump为1,说明子历程是以Core方式退出,形成了core文件。
信号保存
先来看几个概念:
1.现实执行信号的处理动作称为信号递达(Delivery)。
有三种处理方式:默认、忽略、自界说捕获。
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
信号已经产生了,但是还没处理,在历程pcb中进行保存。
3.历程可以选择壅闭某个信号。
壅闭一个信号,那么对应的信号一旦产生,就永不递达,不停未决,直到主动解除壅闭,解除之后,该信号才会被递达。一个信号假如壅闭和它有没有未决无关!!
信号在内核中的表示表示图:
https://i-blog.csdnimg.cn/direct/6cd13e5ff3b144c186cad889305914d0.png
由三张表组成,block、pending、handler。
第一张表pending
其中,pending是一个int位图(0000 0000 0000 0000 0000 0000 0000 0000,最高位不用),比特位的位置表示信号编号,比特位的内容表示信号是否收到,也可以叫做未决信号集。
第二张表handler
我们之前使用过signal指令,用来对指定信号自界说捕获,
https://i-blog.csdnimg.cn/direct/62c375716bfa414b89d63f8ff99abf1e.png
其第二个参数是参数值为int、返回值为void的函数的函数指针,标题是,为什么这样做就能完成信号捕获呢?这就涉及到了handler表,handler表是一个函数指针数组sighandler_t handler,而我们的普通信号是1~31的数字,信号的编号就相当于数组的下标,可以接纳信号编号,索引信号处理方法!OS为每个历程都维护了一张handler表,这个表写了每个信号该如何被处理,未来假如历程收到2号信号,就会看到SIG_IGN所对应的处理方法。学到这里我们应该就明确,之前signal(2,handler)是在干什么呢?着实是拿着信号编号在该handler数组里进行索引,把本身写的函数地址写到handler数组对应的位置,此时OS就知道对于2号信号该怎么处理它。
第三张表block
block和pending类似,也是一张位图,和pending类型完全一样,(0000 0000 0000 0000 0000 0000 0000 0000,最高位不用),比特位的位置表示信号的编号,比特位的内容表示信号是否壅闭。
https://i-blog.csdnimg.cn/direct/8b80465b1b5e4530bd177bb228599c45.png
对于这3张表,我们应该一行一行看,比如对于上图的1号信号,block为0,pending为0,那么就使用handler表中的SIG_DFL递达。对于上图的2号信号,由于block为1,被壅闭,全部当吸收到2号信号时,pending为1,不能被递达,直接打仗壅闭,才能被递达。假设4号信号的对应的block为0,pending为1,表示吸收到了4号信号,由于未被壅闭,可以被递达,去handler表中找对应的方法执行。所以,OS为每个历程维护了两张位图+一张函数指针数组,这样就让历程识别到了信号。我们总结两条结论:
1.被壅闭的信号产生时将保持在未决状态,直到历程解除对此信号的壅闭,才执行递达的动作。
2.留意,壅闭和忽略是差异的,只要信号被壅闭就不会递达,而忽略是在递达之后可选的一种处理动作。
信号相干的操纵,到目前为止我们只学了signal捕获信号,未来对信号的操纵无非就是围绕上面所说的3张表,要么是block表,要么是pending表,要么是handler表,因为内核数据结构是这三个表,那提供的系统调用也是这三个的。因为signal函数可以对handler函数数组进行修改了,所以对signal的操纵难度不大,关键在于block表和pending表之前没有获取过,它俩照旧位图,所以,为了对block和pending表进行操纵,所以Linux提供了一种Linux上的用户级的数据类型sigset_t,把sigset_t这种数据类型叫做信号集,这个类型可以表示可以表示每个信号“有效”和“无效”的状态。sigset_t着实是一个结构体,内里封装了位图。在壅闭信号集中“有效”和“无效”表示信号是否被壅闭,在未决信号集中“有效”和“无效”表示该信号是否处于未决状态。其中,壅闭信号集又叫做信号屏蔽字,这里的壅闭应明白为壅闭而不是忽略。
但是,我们不能本技艺动去修改sigset_t,OS为我们提供了一批函数,
#include <signal.h>
int sigemptyset(sigset_t *set);//把所有位清0
int sigfillset(sigset_t *set); //把所有位置1
int sigaddset (sigset_t *set, int signo); //把指定的信号signo加到位图中,由0置1
int sigdelset(sigset_t *set, int signo);//把指定的信号signo从位图中去掉,由1置0
int sigismember(const sigset_t *set, int signo); //判断signo是否在位图中 sigprocmask
函数sigprocmask可以用来读取或更改历程的信号屏蔽字(壅闭信号集)。先来看一下这个函数的界说:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 其中,sigprocmask函数的第一个参数how有几个选项(SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK),第二个参数set是一个输入型参数,
https://i-blog.csdnimg.cn/direct/f71427b2bfce4982b9abeec0d22544fb.png
然而,无论SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK哪个选项,都是用来修改信号屏蔽字。那假如想恢复成修改之前的信号屏蔽字,该怎么办呢?这就设计到了第三个参数oldset,这是一个输出型参数,在对历程的BLOCK修改之前,都会保存老的信号屏蔽字返回给用户,我们可以不用它,但是当我们想用的时候,就可以重新设置成原来的信号屏蔽字。
上面介绍的是如何修改BLOCK表,那么如何修改pending表呢?现实上,我们之前所讲的信号产生的5种方式,每一种都是在修改pending表,那我们怎么获取pending表呢?
sigpending
int sigpending(sigset_t *set); 我们通过函数sigpending来获取pending位图,其参数set是一个输出型参数。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
void PrintPending(sigset_t& pending)
{
std::cout << "curr process[" << getpid() <<"] pending: ";
for(int signo = 31 ; signo >= 1 ; signo--)
{
if(sigismember(&pending, signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << "\n";
}
void handler(int signo)
{
std::cout << signo << "号信号被递达!!!" << std::endl;
}
int main()
{
//0.捕捉2号信号
signal(2,handler);
//1.屏三蔽2号信号
sigset_t block_set,old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set,SIGINT);
//1.1 设置进入进程的Block表中
sigprocmask(SIG_BLOCK,&block_set,&old_set);//真正修改当前进行的内核block表,完成对2号信号的屏蔽
int cnt = 10;
while(1)
{
//2.获取当前信号的pending信号集
sigset_t pending;
sigpending(&pending);
//3.打印pending信号集
PrintPending(pending);
cnt--;
//4.解除对2号信号的屏蔽
if(cnt == 0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK,&old_set,&block_set);
}
sleep(1);
}
return 0;
} 当解除屏蔽后,假如信号之前被pending了,一般会立即处理当前被解除的信号。那么pending位图对应的信号也要被清0,是在信号被递达前被清0,照旧在信号被递达后被清0?在信号被递达前!
信号处理
什么是信号处理呢?不就是递达信号么?!着实,我们之前信号处理有三种方式:
signal(2,handler);//自定义捕捉
signal(2,SIG_IGN);//忽略一个信号
signal(2,SIG_DFL);//信号的默认动作 那么,SIG_IGN和SIG_DFL详细是什么呢?这着实是宏,我们转到其界说,
https://i-blog.csdnimg.cn/direct/681eabd8de8e44dcae7a3ed40a971b01.png
发现着实是对1和0强转成了函数指针类型,到这里,着实我们应该明确,之所以signal第二个参数要传入这样的函数指针类型,因为handler表是一个函数指针数组。别的,假如不对信号进行处理,那么默认就是执行SIG_DFL。
我们知道,信号大概不会被立即处理,而是在合适的时候进行处理,这里合适的时候着实是指历程从内核态返回到用户态的时候进行处理!这里出现了两个新名词,内核态和用户态。内核态简单来说就是处于操纵系统的状态,用户态就是只要执行我本身的代码、访问我本身的数据,这个时候就叫做用户态。在使用系统调用的时候就进入和内核态,在处理完内核后,先检查当前历程可递达的信号,假如无可递达的信号,那么返回主控制流程继续执行,否则,就处理信号。假如信号对应的handler是SIG_DFL大概SIG_IGN,那么直接执行就好。但是,假如信号是自界说捕获,那就需要按照下图的方式进行。
https://i-blog.csdnimg.cn/direct/786358771edb4bd1a3d021730aa19287.png
但是,操纵系统不能直接转已往执行用户提供的handler方法,因为handler方法是用户提供的,而用户写的方法内里写了什么代码OS并不清楚,万一内里写了rm、exec等执行删除命令了,那这不就是利用OS做坏事了吗?!所以,在执行handler方法时,不能使用内核身份,必须使用用户身份!!OS不信赖托何用户。所以,在进行信号捕获时,要从内核态切换到用户态(3->4步)。但是main函数和sig_handler函数没有调用关系,所以在执行完sig_handler后不能直接跳转会main函数,只能通过特别系统调用返回到内核中(第5步),才能返回main函数。
https://i-blog.csdnimg.cn/direct/88f8eedb6b614d8f92230c6ca3321ecb.png
所以,信号捕获的流程类似∞,信号的捕获过程,要经历4次状态切换!在内核态切换回用户态的时候,进行信号的检测和处理!
内核态和用户态
上面我们肤浅地了解了内核态和用户态,现在我们要更深入明白。为此,我们先要谈几个标题:
再谈地址空间
以32位呆板为例,我们之前学过,GB的地址空间被用户所用(包括堆区、栈区、静态区等),那么还剩下GB的末了的1GB空间是给OS用的。当电脑开机的时候,OS是第一个被加载到内存的软件,那怎么把GB地址空间映射到OS的内存呢?着实是通过内核级页表,这意味着OS本身就在历程的地址空间中!!
https://i-blog.csdnimg.cn/direct/d3645754dc1944278bf4b10975468ed6.png
但是,OS里会有好多历程,这么多历程着实都是用的同一个内核级页表,也就是内核级页表只有需要维护一份,用户级页表可以有许多张。无论历程如何切换,我们总能找到OS!!通过访问GB空间,就可以找到OS全部代码和数据。我们访问OS,着实照旧在我们的地址空间中进行的,和访问库函数没区别!!由于OS不信赖托何人,所以用户访问GB空间时,要受到肯定的约束!只能通过系统调用!!
谈谈键盘输入数据的过程
简单来说,就是当按下键盘后,发生硬件停止,键盘有对应的停止号,发给CPU,然后由CPU关照OS执行对应的读取键盘的方法。
谈谈如何明白OS如何正常运行
1.如何明白系统调用
OS为了支持系统调用,为我们提供了系统调用表,这着实是一个函数指针数组,我们只要找到特定数组下标的方法,就能执行系统调用了。而这个特定数组下标叫做系统调用号。
https://i-blog.csdnimg.cn/direct/b877af3b0311470a966473425063cb6b.png
因此,执行系统调用需要系统调用号和系统调用函数指针数组。当我们使用系统调用(比如fork)时,着实是先把系统调用号放到寄存器里(move 2 eax),然后再执行int 0x80这样的停止,然后在OS内部形成停止号,然后执行提前注册的停止向量表中的方法,然后从寄存器中把停止号读取出来,再执行对应的方法,至此系统调用就可以被调用起来了。
由外部形成的停止叫做外部停止。把CPU中直接形成的可以直接产生停止的叫做陷阱或缺陷(0x80)。
2.OS是如何运行的
OS本质就是一个死循环,从开机之后直到关机OS不停在跑,再加上时钟停止,不停调度系统的任务。
当用户调用系统调用时,首先需要找到OS所在的地址空间。但是,OS不是不信赖托何用户吗?用户无法直接跳转到GB地址空间范围,这是怎么做到的呢?别的,用户必须在特定的条件下才能跳转已往,这又是如何做到的?现实上,要做到这两点,是需要CPU配合的,CPU中有一个叫cs(code semgment)的寄存器,内里表示的是代码区的范围,假如我们想让CPU执行代码区的代码,cs里就放代码区的范围,想让CPU执行OS的代码,cs里就放OS代码的范围(3~4GB)。其中,cs的末了两个比特位假如是0,表示内核,假如是3,表示用户。也就是说,假如是0,那就可以执行OS的代码,假如是0,那就只能执行用户级的代码。所以,所谓的用户态和内核态,指的就是这两个比特位是0照旧3的标题。由用户态切换为内核态,就是把寄存器的值由3变为0,反之相反。
所以现在我们就可以回答第一个标题,因为用户对应的是3,当想直接跳转到OS的系统调用时,CPU肯定先要做状态检查,发现是GB范围内的数据,就会去检测这两个比特位,假如是3不是0,就会拦截,这就访问不到OS的代码了。
现在就可以回答第二个标题,在特定的环境下,比如执行fork调用,会将3->0,这样就能跳转已往。
现在我们回过头看执行流的时候,用户层在执行停止、非常大概系统调用时进入OS,要执行int 0x80停止,把系统调用号放到寄存器中,把cs末了两个比特位由3->0,然后再跳转地址空间,进入OS内执行系统调用,系统调用完再进行信号检测。
好,下面开始另一个话题。在信号捕获时,我们之前说的使用signal调用进行捕获,现在我们再来说另一种方法:sigaction。这看起来复杂一些~。
https://i-blog.csdnimg.cn/direct/a68906b977b24344a3538e37f7d585bd.png
https://i-blog.csdnimg.cn/direct/0010c0def82a48e3b9809813dc2686f4.png
其中,参数signum是要捕获的信号编号,后两个参数是和函数名同名的结构体指针,第二个参数是一个输入型参数,是我们想要设置的信号捕获方法。第三个参数是一个输出型参数,用来保存之前的信号捕获方法,以便恢复原来的信号捕获方法。
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
exit(1);
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
sigemptyset((&act.sa_mask));
act.sa_flags = 0;
sigaction(2, &act, &oact);
while(true)
{
std::cout << "I am a process , pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
} https://i-blog.csdnimg.cn/direct/cac95f0ec79e462082fe42e6ce0a2051.png
我们着实发现,这看起来和signal没有什么区别嘛!
现实上,当前假如正在对2号信号进行处理,默认2号信号会被屏蔽。当对2号信号处理完成的时候,会自动解除对2号信号的屏蔽。这样就避免了对2号信号的一连处理,这里的2号信号可以换成任意特定新号。为什么突然说这个呢?我们看到sigaction结构体里有一个成员是sa_mask,假如想在处理2号信号(此时OS自动屏蔽2号)的时候,同时对其他信号也进行屏蔽。我们使用sigaddset(&act.sa_mask,3);对3号信号也进行屏蔽,这时处理2号时,对2号和3号信号都进行了屏蔽。
可重入函数
https://i-blog.csdnimg.cn/direct/e0ab406843d24870a82ab3fa30899348.png
向上面这样,在insert中执行p->next=head时,假云云时收到信号,进入handler,假如内里照旧调用了insert函数,这样出来之后,就会造成node2内存走漏。在这个过程中,insert函数被重复进入了,也就是被重入了,会出现标题(比如内存走漏),insert函数就叫做不可重入函数。现实上,我们之前学过的大部门函数都是不可重入的。函数是否可重入并不是优缺点,而是特点。
volatile
我们先来看这样一段代码:
int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo:" << signo << ",change flag 0->1" << std::endl;
gflag = 1;
}
int main()
{
signal(2, changedata);
while(!gflag);
std::cout << "process quit normal" << std::endl;
return 0;
} https://i-blog.csdnimg.cn/direct/71492a48c6fa4b0ea398e7672832c8ff.png
我们按ctrl+c(2号信号),会出现如上结果,这是很正常的。
gflag归根结底是存放在物理内存中的,CPU主要负责算数运算和逻辑运算,而while(!gflag)的本质是CPU不停对gflag做检测,CPU把内存中的gflag加载进来逻辑运算然后判断。然而,编译器大概对我们上面这段代码进行优化。Linux中,对代码的优化有几个级别:
https://i-blog.csdnimg.cn/direct/cf2ce92ff5d0470ab62f7be99fa33a77.png
默认是O0(无优化),优化级别依次递增。
假如把优化级别拉到-O1,对编译器在编译时,会发现main函数中没有对gflag进行修改,编译器就想我没必要每次都从内存中拿数据呀,直接从内存中拿一次gflag的值,以后就只检测寄存器的值,假如以后代码对gflag修改了,这着实是修改内存里的值,和CPU无关了,CPU再也看不到了,也就是寄存器隐藏了内存中的真实值。这就是编译器过分优化导致的标题。
https://i-blog.csdnimg.cn/direct/35bbd4610b9847e19dd4281d9141f953.png
为了要求CPU每次都必须从内存读取数据,即保持内存的可见性,就提供了volatile关键字,用volatile修饰gflag。
SIGCHLD
现实上,子历程在退出时,不是静悄悄退出的,会给父历程发送信号--SIGCHLD。那为什么之前子历程退出时看到信号呢?是因为SIGCHLD是Ign的。
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
pid_t rid = waitpid(-1, nullptr, 0);
if(rid > 0)
{
std::cout << "wait child success,rid: " << rid << std::endl;
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
pid_t id = fork();
if(id == 0)
{
//child
std::cout << "I am child process,pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
//father
while(true)
{
DoOtherThing();
sleep(1);
}
return 0;
} https://i-blog.csdnimg.cn/direct/93aa1a455c574ec0968c4311cdd7e4a5.png
这样,父历程可以用心做本身的事,当子历程退出时,会自动向父历程发送SIGCHLD信号,在回收完子历程后,父历程就可以继续做本身的事。
我们来对上面的代码挑标题:
标题1:假如一共有10个子历程,且同时退出呢?
假如10个子历程同时退出,这就意味着在很短时间内子历程退出时都会向父历程发送SIGCHLD信号,而SIGCHLD作为普通信号,是用pending位图来表示收到的信号的,虽然同时收到10个SIGCHLD信号,但pending只会记载一次,就只会回收一个子历程,那怎么办呢,我们可以这样做:
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
while (true)
{
pid_t rid = waitpid(-1, nullptr, 0);
if (rid > 0)
{
std::cout << "wait child success,rid: " << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success done" << std::endl;
break;
}
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
std::cout << "I am child process,pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
}
// father
while (true)
{
DoOtherThing();
sleep(1);
}
return 0;
} 这样就一次创建了10个子历程,并且一次回收了10个子历程。
标题2:假如一共有10个子历程,5个退出,5个永远不退出呢?
会发生壅闭!!!就会不停停在notice里,main函数也不停不会被返回,也不会并发执行DoOtherThing,因此,waitpid时应接纳非壅闭方式,
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
while (true)
{
pid_t rid = waitpid(-1, nullptr, WNOHANG);//非阻塞方式等待
if (rid > 0)
{
std::cout << "wait child success,rid: " << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success done" << std::endl;
break;
}
else // rid == 0 还有部分子进程未退,所以不用回收了,退出即可
{
std::cout << "wait child success done" << std::endl;
break;
}
}
} 现实上,要想不产生僵尸历程还有别的一种办法:父历程调用sigaction将SIGCHLD的处理动作
置为SIG_IGN,这样fork出来的子历程在终止时会自动清算掉,不 会产生僵尸历程,也不会关照父历程。
int main()
{
signal(SIGCHLD, SIG_IGN);//手动设置对SIGCHLD进行忽略即可
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
std::cout << "child process running" << std::endl;
cnt--;
sleep(1);
}
exit(1);
}
while(true)
{
std::cout << "father running" << std::endl;
sleep(1);
}
return 0;
}
https://i-blog.csdnimg.cn/direct/29037095a3d9408489c803c4cd6e7de1.png
https://i-blog.csdnimg.cn/direct/146d49dfb2284ff08efb4bebb46d043d.png
那是不是我们以后直接把子历程的退出信号忽略这样做就行了,别忘了,等待子历程有两个目标,其一是获取子历程退出信息,其二是回收子历程,所以假如根本不关心子历程的退出信息,那直接这样用是最简单的。但是假如想要获取子历程的退出信息,那么就必须要wait。假如细心的话,我们发现SIGCHLD本身就是Ign的,那这里为什么还设置了SIG_IGN,着实,这两个寄义是差异的,Ign是系统维护的,用户设置了SIG_IGN表示用户不用去回收子历程了,会自动释放子历程。
,
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]