目录
1. 掌握Linux信号的基本概念
2. 掌握信号产生的一般方式
第一种 键盘产生
证明:
键盘ctrl+c 的时候,本质是我们向指定进程发送2号信号!
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signal)
{
printf("get a signal :signal no:%d pid:%d\n ",signal,getpid());
exit(1);
}
int main()
{
//通过signal注册对2号信号的处理动作,改成我们的自定义动作
signal(2,handler);
while(1){
printf("hello world!,pid: %d\n",getpid());
sleep(1);
}
return 0;
}
信号的产生方式其中一种就是通过键盘产生(键盘产生的信号,只能用来终止前台进程)
总结:
一般而言,进程收到信号的处理方案有三种情况
- 默认动作 — 一部分是终止自己,暂停等
- 忽略动作 — 是一种信号处理的方式,只不过动作就是什么也不干
- (信号的捕捉)自定义动作 — 我们刚刚用signal方法,就是再修改信号的处理动作,由:默认-》自定义动作
第二种 进程异常,也能产生信号
证明:
core dump是,并获取?
补充:
关于core dump
当你在重新启动进程,或者新开窗口至当前文件,等,,,
你的ulimit -c 会被重新置为0
此时需要你ulimit -c 10240 重新设置大小
第三种 通过系统调用,产生信号
实验:
采用系统调用,向目标进程发送信号
延伸:
int kill(目标进程pid,信号)
int raise(给自己发送信号)
成功0 错误-1
延伸:
abort()
给自己发送6 号信号,并终止进程
第四种 软件条件,也能产生信号
通过某种软件(OS),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景下,触发的信号发送
例如:
进程间通信:当读端不光不读,而且还关闭了读文件描述符,写端一直在写,最终写进程会收到sigpipe信号(13),就是一种典型的软件条件触发的信号发送
介绍接口alarm:
通过调用 alarm,告诉系统 seconds秒后给我发一个14号信号,
你设置了seconds秒后提醒你,,但是由于别的原因,OS给你了一个14号信号,此时这个函数返回二者差值
count++:
只在cpu上运行,所以速度块
printf在IO上运行,所以慢
所以当我们的程序出现大量IO时,要考虑效率问题
总结:
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终,一定都是通过OS向目标进程发送的信号!!!
产生信号的方式,本质都是OS发送的!
如何理解OS给进程发送信号?
目前我们理解:OS 发送信号数据给task_struct
3. 理解信号递达和阻塞的概念,原理。
1.信号其他相关常见概念
2. 在内核中的表示
block pending bandler
推出:进程是可以识别信号的
pending:保存的是,已经收到信号,但是还没有被递达的信号
OS发送信号的本质:修改目标位置的pending 位图
衍生:block:状态位图,表示那些信号不应该被递达,直到接触阻塞!
handler:函数指针数组,【31】,每个信号的编号就是该数组的下标
3. sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,
这个类型可以表示每个信号的“有效”或“无效”状态,
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,
而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
虽然sigset_t是一个位图结构,但是不同的OS实现是不一样的,不能让用户直接修改该变量
需要使用特定的函数
set是一个变量,该变量在什么地方保存?
和我们之前用到的int,double,没有任何差别,都是在用户栈上
sigset_t set;
4. 信号集操作函数
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);
sigprocmask
修改的是进程的block位图
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
结论:
在我们的信号中,9号信号,管理员信号,不能被屏蔽的,不能被自定义捕捉,必须永远遵守默认行为
sigpending
不对pending位图做修改,而只是单纯的获取进程的pending位图
问题:
pending位图谁修改?OS
测试:
先屏蔽2号信号
不断地获取当前进程的pending位图,并打印显示
然后手动发送2号信号,因为2号信号不会被递达
所以,不断地获取当前进程的pending位图,并打印显示
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void show_pending(sigset_t *set)
{
int i = 1;//有些linux下不支持在for里面定义变量
printf("curr process pending(pending位图):");
for(;i<=31;i++)
{
if(sigismember(set,i)){
printf("1");
}
else printf("0");
}
printf("\n");
}
int main()
{
sigset_t iset,oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset,2);
sigprocmask(SIG_SETMASK,&iset,&oset);
sigset_t pending;
while(1)
{
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);
sleep(1);
}
return 0;
}
代码描述;
执行之后,会每间隔一秒打印一长串数组(pending位图)
当你在另一个窗口发送 kill -2 程序
pengding位图中第二个数字就会变为1
4. 掌握信号捕捉的一般方式。
信号发送后,不是被立即处理的,而是在合适的时候。
“合适”是什么时候?
为什么是“合适的时候”?
信号的产生是异步的,当前进程可能正在做更重要的事情,
信号延时处理(取决于OS和进程)
信号什么时候被处理?
因为信号是被保存在进程的PCB中,pending位图里面,处理(检测,递达(默认,忽略,自定义))
当进程从内核态 返回 到 用户态 的时候,进行上面的检测并处理工作!
内核态和用户态
感性认识:
内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行全部都是在内核态!
用户态:就是用户代码和数据被访问或者执行的时候,所处的状态,我们自己写的代码全部都是在用户态执行的!
主要区别:在于权限
系统调用:open函数的实现
用户调用系统函数的时候,除了进入函数,身份也会发生变化,用户身份变成内核身份
较为理性的认识
用户的身份是以进程为代表的
用户的数据和代码一定要被加载到内存
那么OS的数据和代码呢??也是一定要被加载内存中的!
OS的代码是怎么被执行的呢??只有一个CPU!
进程具有了地址空间是能够看到用户和内核的所有内容的,不一定能访问
用户使用的是,用户级页表,只能访问用户数据和代码
内核态使用的是内核级页表,只能访问内核级的数据和代码
CPU有寄存器保存了当前进程的状态
进程之间无论如何切换,我们能够保证我们一定能够找到同一个OS,因为我们每个进程都有3-4G的地址空间,使用同一张内核页表。
所谓的系统调用:就是进程的身份转化称为内核,然后根据内核页表找到对应的系统函数,执行就行
在大部分情况下,实际上我们OS都是可以在进程的上下文中直接运行的
默认:
终止进程,将进程相关资源全部释放掉
暂停, 将进程状态设置为stop,将进程PCB放到等待队列中
忽略:
将pending由1变为0,然后直接返回至下一行
自定义捕捉:(见上下图线)
问题:
为何一定要切换成为用户态,才能执行信号捕捉方法?
OS能否直接执行用户的代码呢?理论是可以的
OS不相信任何人!OS因为身份特殊,不能直接执行用户的代码!
总结:
什么是“合适”的时候?
从内核切换回用户态的时候,进行信号检测与信号的处理!
信号捕捉方式
1.signal
2.sigaction(和signal作用是一摸一样的)
修改的是handler函数指针数组
也可以直接对
act.sa_handler=SIG_IGN;忽略
SIG_DFL默认-----如果收到二号信号就会直接终止
上述代码是将2号信号进行自定义为 输出一段话
我们还可以顺便将其他的信号 自定义为这个同类型的
18:初始化act
19:给act的sa_mask添加3号信号
结果:
3号信号和我们修改后的2号信号将会一样
延伸:
我们将2号信号屏蔽了,然后给进程发送100个二号信号,此时我们的进程只能记住一个。
所以:我们的linux本身普通信号是可能丢失的,这里的丢失指的是2个以上的会被丢失,
实时信号是不可能丢失的,实时信号本身在linux内核中使用链表队列的形式将所有的实时信号,结构体链接到队列中,来一个链接一个,所以不会丢失,本质上还是底层数据结构的差别。
5. 重新了解可重入函数的概念。
此时node2:节点丢失/内存泄漏
这种现象是:insert函数被重复进入了!
insert函数一旦重入,有可能出现问题—该函数不可被重入
insert函数一旦重入,不会出现问题—该函数可被重入
即:重入或者不可重入是用来描述函数的特点的
我们所学的大部分函数,STL,boost库中的函数,大部分都是不可重入的!
volatile
我们发现编译器它本身在经过一定的优化的时候,会对同一份代码展现出来了不同的结果。
首先,我们不能因为这一份代码而认为编译器不应该进行优化,或者认为优化错误。
应该理解为:编译器优化并不能甄别出代码当中O执行流这样的情况,
一般情况下这里的flag是一个全局变量,是变量就应该在内存中开辟空间,cpu要识别flag一定是在内存中读flag,读到cpu内,在cpu内做判断,判断完后,然后再在从内存中读取flag,不断的在内存中读取,来检测flag。
但是编译器在main函数中发现没有人在main中对flag进行修改,所以就直接优化flag到寄存器当中,以后不在进行内存级别的访问,直接识别cpu内的寄存器相关的信息,这个没有问题,这是编译器应该做的。
但是在我们今天的代码中就会出现问题。该如何解决这个问题?
提出问题:为什么?volatile做了什么?
volatile作用1:告诉编译器,不要对我这个变量做任何优化,读取必须贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据!
即:保存内存的可见性!
6. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制
1.子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略
2.不回收进程,不waitpid,子进程退出的时候直接退出就行,也不要形成僵尸进程
这种情况只在Linux下有效,其他平台不知道