区分信号和信号量:
生活中的信号:红绿灯;烽火;线上购物商品;外卖等
信号:本质是一种通知机制,用户或操作系统通过发送一定的信号,来通知进程某些事件已经发生,你可以在后续进行处理
kill -9
杀死一个进程,本质就是对进程发送了9号信号来杀死进程,此处“9”就是一个信号ctrl+c
本质就是通过键盘组合键向目标进程发送2号信号 --> 进程退出kill -l
Linux中对应的所有的信号:普通信号和实时信号(标绿为相对常见)
共62个信号(没有32,33,没有0号信号):[1:31]
信号被称为普通信号 ;[34:64]
信号中带有RT的称为实时信号
实时操作系统:有严格的时序,需要立马严格地响应、处理完成。
分时操作系统:基于时间片的轮转调度算法(一个时间片上的时间乘以用户数,前者一定,后者越多,响应时间就越长)的操作系统
如:汽车中的车载操作系统,有些操作系统是Linux,也会参与我们的汽车的操作(如刹车)。若是分时操作系统,刹车进程没货的处理机的处理,就不会马上刹车;若是实时操作系统,刹车就会立即执行。
1.SIGHUP
挂起
2.SIGINT
:中断/终止进程
3.SIGQUIT
:退出
5.SIGTRAP
和6.SIGTRAP
:终止
8.SIGFPE
:浮点不透?
9.SIGKILL
:捕捉信号9号信号
10.SIGUSR1
:用户自定义信号
11.SIGSEGV
:段错误对应的错误
13.SIGPIPE
:向已关闭的管道中写入可能会出错误
14.SIGALRM
:闹钟
value
:认识信号的编号action
:默认处理行为 ==> 信号对应的动作
0000 0100
表示第三个信号产生task_struct
)的,而task_struct
是内核数据结构(操作系统内的数据结构),故只有操作系统才有资格修改内核数据结构。==> 即,信号产生方式多种,但都是操作系统发的(仅有OS有资格修改PCB内部的相关字段)signal
命令#include
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
用途:通过回调的方式,修改对应的信号捕捉方法
signum
:对哪一个信号进行捕捉
handler
:函数指针类型的参数
void(*sighandler_t)(int)
:返回值是void,参数是int的函数指针
$ ulimit -a
:查看当前环境中相关资源配置的问题。
mysignal
相关使用:
#include
#include
#include
using namespace std;
void catchSig()
{
cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl
}
int main()
{
//signal一般是写在main函数开头
//“ctrl+c”是2号信号SIGINT是键盘产生的
//signal(SIGINT, fun);//a.可直接写名(推荐,可读性好)
signal(2, fun); //b.也可写编号
signal(SIGINT, catchSig); //分析点①
signal(SIGQUIT, catchSig);//“ctrl+\”是3号信号SIGQUIT
while(true)
{
cout << "我是一个进程,我正在运行..., Pid: " << getpid() << endl;
sleep(1);
int a = 100;
a /= 0;//除0后会触发8号信号
cout << "run here ...." << endl;
}
return 0;
}
步骤1:查看是否已核心转储形成core文件
$ ll
core.11077
……
步骤2:
$ gdb mysignal # 可执行文件
(gdb) core-file core.11077
步骤3:
# 会自动定位到终止的错误代码位置
kill
接口#include
#include
int kill(pid_t pid, int sig);//pid指定进程;sig指定信号
#include
#include
#include
#include
#include
#include
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}
// ./mykill 2 pid ==>三个参数
int main(int argc, char* argv[])
{
if(argc != 3) //命令行参数argc不为3,就是传参错误
{
Usage(argv[0]);
exit(1);
}
int signumber = atoi(argv[1]);//①发几号的信号:0是可执行程序名字,1是第一个选项(即"./mykill 2 pid"中的第二个参数)
int procid = atoi(argv[2]);//②给哪个进程发信号
kill(procid, signumber);//给谁发什么信号
return 0;
}
raise
接口#include
int raise(int sig);
#include
#include
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
cout << "我开始运行了" << endl;
//调用方式①
//sleep(1);
//raise(8); // = raise(6) = kill(getpid(), 6)
//调用方式②
abort(); // 自己给自己发6号信号,通常用来进行终止进程(终止进程:exit/return/abort)
return 0;
}
13号信号SIGPIPE
的方式
13号信号SIGPIPE
alarm
函数SIGALRM
信号, 该信号默认处理动作是终止当前进程。#include
unsigned int alarm(unsigned int second
#include
#include
#include
#include
#include
using namespace std;
uint64_t count = 0;
// 实现基本定时器功能
void catchSig(int signum)
{
cout<<"final count : "< 这俩个都是阻塞式IO
/*int count = 0;
while (true)
{
cout << "count:" << count++ << endl;
}*/
// 2. 单纯计算算力
signal(SIGALRM, catchSig);
while(true) count++;//纯计算,无IO
return 0;
}
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef function func;
vector callbacks;
uint64_t count = 0;
void showCount()
{
cout << "final count : " << count << endl;
}
void showLog()
{
cout << "这个是日志功能" << endl;
}
void logUser()//定期查看用户是谁
{
if(fork() == 0)//若是子进程
{
execl("/usr/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
// 此就可以用于处理数据自动定时刷新等任务
// void flushdata()
// {
// }
void catchSig(int signum)
{
for(auto &f : callbacks)
{
f();
}
alarm(1);
}
int main(int argc, char* argv[])
{
alarm(1);
signal(SIGALRM, catchSig);
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(logUser);
while(true) count++;
return 0;
}
test_struct*
,该指针定义了一个current
指向的当前正在运行的进程pcb。当进程被调度的时候,current
指针内的内容也会load到CPU内其他相关的寄存器using namespace std;
void handler(int signum)
{
sleep(1);
cout << "获得了一个信号: " << signum << endl;
// exit(1);
}
int main(int argc, char* argv[])
{
//①除0异常
signal(SIGFPE, handler);
int a = 100;
a /= 0;
//②越界、野指针问题在Linux中都可称段错误==>对应11号SIGSEGV信号
signal(SIGSEGV, handler);//捕捉到SIGSEGV就执行handler()
int* p = nullptr;
*p = 100;//p解引用后是指向0号地址,往没有写权限的0号地址写属于段错误
while (true) sleep(1);
return 0;
}
所有的信号都有它的来源,但最终全部都是被OS识别、解释并发送的
问题:
0000 0001
:代表收到了1号信号;0000 0000
:代表没有收到1号信号typedef(*handler_t)(int);
handler_t handler[32];
signal(signum,handler);
:调用signal时,并非直接调用handler方法,而仅是将自定义的捕捉动作方法填入到handler数组下标里,真正的执行调用在后续才被实现。识别信号并根据对应动作执行的过程如下:取到信号编号signal
handler[signal];
(int)handler[signal]==0;//执行默认动作,done
(int)handler[signal]==1;//执行忽略动作,done
handler[signal]();//当前两个动作都不满足时,才回去调用对应的方法
①OS向目标进程发送信号:即修改pending位图
②进程在合适时间处理信号:遍历检测pending位图,当找到为1的比特位时 ==> 先去block表查看当前信号是否被阻塞,若为1被阻塞则不处理当前信号;若为0,才进入handler表调用处理对应信号的方法
提高计算机学习认知
sigset_t
回顾以前学的操作系统类型:pid_t
基本上,语言会提供.h
, .hpp
和语言的自定义类型; 同时OS也会提供.h
和OS自定义的类型
.h
内部接口相对应(相当于系统调用接口). 有的接口不允许用户去传语言层的参数, 而是需要传结构体/位图等,而既然OS提供了对应接口,就必须也提供对应的类型.h && .hpp
)一定会包含OS提供的头文件(.h
)以及OS自定义的类型sigset_t
属于位图结构, 也属于OS提供的一种数据类型
sigset_t
不允许用户自己进行位操作, OS有提供对应的操作位图的方法 ==> [[7.进程通信#4 信号集操作函数]]sigset_t
类型,和用内置类型&&自定义类型没有任何差别sigset_t
一定需要对应的系统接口来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t
定义的变量或者对象由[[7.进程通信#2 信号在内核中的表示]]可见,每个信号只有一个bit的未决标志(0/1)和一个bit的阻塞标志,且并不记录该信号产生了多少次。
sigset_t
位图结构,把所有信号放一起为集合的形式),这个类型可以表示每个信号的“有效”或“无效”状态。
#include
int sigemptyset(sigset_t *set); //清空:全置0
int sigfillset(sigset_t *set); //全置1
int sigaddset (sigset_t *set, int signo); //将特定信号添加至信号集中(设置信号集中比特位)
int sigdelset(sigset_t *set, int signo); //从信号集当中删除对应信号
int sigismember(const sigset_t *set, int signo); //判定某个信号是否在信号集中
#include
int sigpending(sigset_t *set);
#include
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
int how
可选值如下:
SIG_BLOCK
:set包含要添加到当前信号屏蔽字的新信号 ==> mask = mask|set
SIG_UNBLOCK
:set包含了要从当前信号屏蔽字中解除阻塞的信号 ==> mask=mask&~set
SIG_SETMASK
:设置当前信号屏蔽字为set所指向的值 ==> mask = set
sigset_t *oldset
:输出型参数,用于返回旧的信号屏蔽字。
nullptr
*oldset
参数是给原信号屏蔽字做备份,以备对旧信号的不时之需//makefile文件
mysignal:mysignal.cc
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f mysignal
//mysignal.cc文件
#include
#include
#include
void catchSig(int signum)
{
std::cout << "获取一个信号: " << signum << std::endl;
}
int main()
{
for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig);
while(true) sleep(1);
return 0;
}
(void)n;
:此语句是为了消除编译器的警告的习惯,比如定义了一个变量后面没有使用,编译器会提醒没使用。而将变量n强制转换为没有返回值的表达式,可以抑制编译器对变量的警告信息。
#include
#include
#include
#include
static void showPending(sigset_t& pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))//在pending集合里
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
static void blockSig(int sig)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset, sig);
int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
assert(n == 0);
(void)n;
}
int main(){
//0~4步实现对2号进程进行阻塞
// 0. 方便测试,捕捉2号信号,不要退出 -- 分析点①
signal(2, handler);
// 1. 定义信号集对象
sigset_t bset, obset;
sigset_t pending;
// 2. 初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&bset, 2 /*SIGINT*/);
// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);//在release下不可用assert
(void)n;//分析点③
std::cout << "block 2 号信号成功... , pid: " << getpid() << std::endl;
// 5. 重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中的没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 20)
{
//分析点①
std::cout << "解除对于2号信号的block" << std::endl;//分析点②
int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
assert(n == 0);
(void)n;
}
}
return 0;
}
#include
#include
#include
#include
static void handler(int signum)
{
std::cout << "捕捉 信号: " << signum << std::endl;
// 不要终止进程,exit
}
static void showPending(sigset_t& pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
static void blockSig(int sig)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset, sig);
int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
assert(n == 0);
(void)n;
}
int main()
{
for (int sig = 1; sig <= 31; sig++)
{
blockSig(sig);
}
sigset_t pending;
while (true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
信号相关的数据字段都是在进程PCB内部,而PCB内部属于内核范畴
为什么会进入内核态?
如何进入内核态?
int(汇编指令,取“中断interrupt”的前三个字母) 80
,将代码的执行权限由我下达给操作系统,让操作系统去执行int 80
:内置在系统调用函数中(可理解做系统调用函数内部就有int 80
)int 80
,将CPU中寄存器中3用户态改变至1内核态 ;而每个系统调用函数内部都存在int 80
指令int 80
指令,将3用户态转换成1内核态,内核态允许访问,从而通过OS的权限审查,而后跳转到操作系统的内核地址空间,使用内核级页表,因此可以访问操作系统内的所有资源。为什么要从用户态切换到内核态?
内核态与用户态的切换是如何做到的呢?
为什么要从内核态切换到用户态?
为什么是从内核态返回用户态时进行信号检测处理,而不是从用户态刚进内核态就处理呢?
atexit
),会用于在进程终止之前,帮你返回用户态,而后执行特定的方法sigset_t
传参时,是需要将用户层数据拷贝到内核的,若内核无法访问用户,它是无法完成拷贝的;sigreturn
,从而再次进入内核态。再在内核态中,完成将pending比特位由1置0、恢复函数的栈帧结构等,而后返回main函数,继续向后执行,至此完成完整的信号捕捉动作。1、信号捕捉调度变化的逻辑理解链
2、信号捕捉身份转换的理解链
int signo
: 对应信号的编号const struct sigaction *act
: 一种结构体[[7.进程通信#sigaction 结构体介绍]],操作系统提供的数据类型.在C/C++中结构体类型名称和函数的名称允许是同样的(但并不建议这么处理).sigaction类型是一种输入型参数,此参数(sigaction类型的结构体)中包含你对该信号要怎么处理的回调函数struct sigaction *oldact
: 返回对信号的旧处理方法,属于输出型参数.#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
sigaction
结构体介绍struct sigaction{
void (*sa_handler)(int); //sa_handler方法即对应信号捕捉的回调函数
void (*sa_sigaction)(int, siginfo_t *, void *);//×暂时不考虑
sigset_t sa_mask; //√重点谈论
int sa_flags; //×暂时不考虑
void (*sa_restorer)(void);//×暂时不考虑
};
void (*sa_handler)(int);
sa_handler
赋值为常数SIG_IGN
传给sigaction表示忽略信号;赋值为常数SIG_DFL
表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,信号捕捉,并没有创建新的进程或线程
重入函数:一个函数在一个特定时间段内被多个执行流重复进入,此函数称为“重入函数”
可重入函数和不可重入函数:属于函数的一种特征,并无好坏之分。
目前我们使用的90%函数,都是不可重入的,如果一个函数符合以下条件之一则是不可重入的:
我们无法假设别人的编译场景,编译器有时候会自动进行代码优化:
-O1
,-O3
是优化级别最高的。volatile
关键字作用演示
!flag
)!flag
),并无修改flag的语句。 ==> 于是在编译器优化里(-O3
优化),编译器第一次将flag的默认值0放入edx后,再做while循环检测时,它自作主张仅在cpu里重复对第一次读取到的flag做检测。 > 可当信号来时(signal(2,changeFlag);
),即使修改了内存中flag=1
,cpu也并不再访问内存里flag,依旧检测的是首次读到的flag值,导致了数组二异性。> 故优化的存在,让cpu无法看到内存了volatile int flag =0;
),就需要程序员显性地告诉编译器不要给它优化(如对于下例中的while(!flag);
,必须去内存中访问 ==> 保持内存的可见性)int flag =0;//volatile int flag =0;
void changeFlag(int signum)
{
(void)signum;
cout << "change flag:" << flag;
flag = 1;
cout<<"->"<
volatile
的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量(如上例中的:volatile int flag =0;
),不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作回顾:[[4.Linux进程控制#1、进程等待的必要性 功能]]子进程在退出后,父进程需要对其wait,否则会造成以下两点问题
而父进程处理子进程结束有两种方式:
轮询过程:子进程在退出或暂停时,会主动地通过OS,向父进程发送17号SIGCHLD
信号。
SIGCHLD
信号默认动作是忽略(应用层忽略是什么都不做;在内核中,OS可能还会对忽略再做些工作)。SIGCHLD
信号” 的方案非阻塞处理的不足及优势
回收退出子进程的新思路:既然Linux采用了发送SIGCHLD
信号的方式通知父进程,父进程就不必主动去等待子进程了,而是当子进程退出时,通过设置捕捉信号,在捕捉方法里对信号做回收,就能完成一套基于信号版的子进程回收
SIGCHLD
信号//证明子进程退出,会向父进程发送17号SIGCHLD信号
void handler(int signum)//父进程执行信号捕捉handler
{
cout <<"子进程退出: "<< signum << endl;
cout << "father pid: " << getpid() << endl;//若输出17号新号编码,则证明确实子进程退出发出了17号SIGCHLD信号
//父进程进行信号捕捉,故可在此回收退出的子进程
wait();//若有10个子进程都退出,则采用while(wait())
}
int main()
{
signal(SIGCHLD, handler);//捕捉子进程退出发出的SIGCHLD信号
if(fork() == 0)//子进程退出
{
cout<< "child pid: " << getpid()< 取决于是否需要父进程退出 ==> 取决后面的代码需求
……
}
//最后输出
// child pid: 27251
// 子进程退出: 17
// father pid: 27250
子进程退出,父进程是会接收到子进程退出发送的SIGCHLD
信号,但:
若一共10个进程,其中有5个子进程退出,当while循环出前5个都退出发送了SIGCHLD
信号,那还要继续while循环第6个子进程吗?
SIGCHLD
信号,仅会接收到1个,其余可认为均丢失,因为:
SIGCHLD
信号的bite位仅有1个,它并没有传达信号数量的信息。SIGCHLD
信号,实际上只收到了1个信号(操作系统只会保留一个),而剩下的4个信号可认为已丢失了SIGCHLD
信号,但其实并不能确定有几个进程退出,所以只能用while循环不断去读取。waitpid和wait
本身除了回收,还可以检测子进程是否退出。==> 因为我们无法确定进程退出的数量是5个还是6个,故仍需通过while循环继续检测while循环+vector pids数组
:在fork时,采用vector pids
数组记录所有子进程pid,而后去非阻塞遍历所有进程,再在对应代码里对其进行捕捉检测,从而回收退出的子进程while( (id = waitpid(-1, NULL, WNOHANG))
:waitpid中传入-1,效果等价于wait。作用是:我们可以等待任何一个退出的进程,不关心是哪个,但若是-1,操作系统就会去识别哪个进程退出了;若没有任何一个进程退出,调用-1也会导致阻塞#include
#include
#include
using namespace std;
int main()
{
//手动设置对子进程进行忽略(应用层的忽略)
signal(SIGCHLD,SIG_IGN);//分析点①
if(fork()==0)
{
cout<<"child: "<