专栏分类:C语言初阶 C语言进阶 数据结构初阶 Linux C++初阶算法 C++进阶
欢迎大家点赞,评论,收藏。
一起努力,一起奔赴大厂
一. 信号的引入
1.1提炼基本概念
- 信号在生活中随时可以产生,信号的产生和我是异步的;
- 信号的产生后你能认识这个信号吗?
- 知道信号产生了,信号该如何处理 ?
- 当我们正在做其他重要的事,把来到的信号暂时不处理,做完事情后我得记得这个事,什么时候处理这个事?合适的时候。
1.2信号概念的基础准备
信号:Linux系统提供的一种,向指定进程发送特定事件的方式,对其进行识别和处理,信号的产生是异步的(异步就是各干各的)。信号有哪些呢??可以输入指令:
cpp
kill -l
目前有64个信号,每一个信号都是位图的一位,我们经常使用的
cpp
kill -9 pid
运行代码:
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "hello signal,my pid :" << getpid() << std::endl;
sleep(1);
}
return 0;
}
9号信号发送结果如图:
1.3信号处理
信号的处理有三种方式
- 忽略
- 默认
- 自定义捕捉
在自定义捕捉中有一个系统调用
cpp
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
它的第一个参数是哪一个信号,第二个参数是一个函数指针类型,它代表接受到signum信号就会执行handler。
1.3.1 ctrl+c和2号命令
先看代码:
cpp
\#include <iostream>
\#include <signal.h>
\#include <sys/types.h>
\#include <unistd.h>
void handler(int sig)
{
std::cout << "my sig is : " << sig << std::endl;
}
int main()
{
signal(2, handler);
while (true)
{
std::cout << "my pid is : " << getpid() << std::endl;
sleep(1);
}
return 0;
}
运行结果如图:
其实2号命令还可以通过键盘来产生,输入
cpp
ctrl+c
也是同样的效果
1.3.2 ctrl+\和3号命令
3号信号是
cpp
ctrl+\
不过不在这里进行演示。
二 . 信号的产生
信号从产生到处理共有三个阶段,第一个阶段就是信号的产生,其中信号的产生有五种
2.1通过kill命令向指定进程发送信号
就是1.2的kill命令的使用,这里不做解释
2.2通过键盘产生信号
就是1.3的ctrl+c和ctrl+\这两个命令,他们分别对应2号和3号命令,这里不做解释。
2.3系统调用
2.3.1 kill
cpp
int kill(pid_t pid, int sig);
第一个参数是进程的pid,第二个参数是给进程pid发送的信号。代码如下:
cpp
//main.cc
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
int main(int argc,char*argv[])
{
if(argc!=3)
{
std::cout<<"Usage:"<<argv[0]<<", you need give signal and pid"<<std::endl;
exit(1);
}
int signal=std::stoi(argv[1]);
int pid=std::stoi(argv[2]);
std::cout<<"signal: "<<signal<<" pid: "<<pid<<std::endl;
kill(pid,signal);
return 0;
}
//test.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "my pid is : " << getpid() << std::endl;
sleep(1);
}
return 0;
}
运行结果如下:
2.3.2 raise
cpp
int raise(int sig);
这个系统调用就是谁使用就给谁发送sig信号。看代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "my sig is : " << sig << std::endl;
}
int main()
{
signal(2,handler);
while (true)
{
raise(2);
sleep(1);
}
return 0;
}
运行结果如下:
2.3.3 abort
cpp
void abort(void);
这个系统调用就是给使用的进程发送6号信号。看代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "my sig is : " << sig << std::endl;
}
int main()
{
signal(6,handler);
for(int i=0;i<20;i++)
{
if(i==7)
{
abort();
}
std::cout<<"hello abort"<<std::endl;
sleep(1);
}
return 0;
}
运行结果如下:
2.4 软件条件(闹钟)
例如在进行进程间通信时的***管道***,它的读端关闭就会发送13号信号。还有一种就是一个**闹钟**
cpp
unsigned int alarm(unsigned int seconds);
有这个闹钟,我们需要知道三件事:
2.4.1 IO很慢
根据闹钟,我们可以设定为1秒,当时间到后闹钟会发送14号信号,所以闹钟响后我们退出程序,输出一下count,另一个代码是一直输出count,1秒后停止,看代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int count=0;
void handler(int x)
{
std::cout<<"count: "<<count<<std::endl;
exit(1);
}
int main()
{
signal(14, handler);
alarm(1);
while(true)
{
count++;
}
return 0;
}
运行结果为
另外一个代码:
cpp
int main()
{
//signal(14, handler);
alarm(1);
while(true)
{
count++;
std::cout<<count<<std::endl;
}
return 0;
}
运行结果为:
可以看到当进行输出时只有14万,但是直接加却有5亿多,所以可以看出IO真的很慢。
2.4.2 理解闹钟
闹钟可以在操作系统中存在多个,所以操作系统需要对闹钟进行管理,所以是**先描述在组织**,闹钟是一个结构体,里面有未来的超时时间,进程pid等,它是如何组织呢?通过最大堆最小堆,只要看看堆顶元素是否超时就可知道其余的是否超时。在硬件上,计算机内部会有一个时间戳,这样就让计算机即使没有电也能知道时间。看下面代码是对闹钟的一些操作:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
alarm(5);
sleep(2);
int n=alarm(0);
std::cout<<"n: "<<n<<std::endl;
return 0;
}
看运行结果:
其中alarm(0)就是说让闹钟立刻响,返回上一个闹钟的剩余时间,闹钟只默认触发一次。
再例如第一个闹钟设为10,sleep(4)秒,n=alarm(2),此时的n为6。
2.5异常
这里的异常和硬件有关,主要包括除0和野指针的非法访问。
2.5.1 除0
先看代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
int a=1/0;
while(true)
{
std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
}
return 0;
}
代码运行如下:
cpp
Floating point exception (core dumped) //8号信号
对8号信号进行捕捉,然后看效果,代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout<<"hello, my pid :"<<getpid()<<" sig: "<<sig<<std::endl;
sleep(1);
}
int main()
{
signal(8,handler);
int a=1/0;
while(true)
{
std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
}
return 0;
}
运行结果如下:
我们只有一次除0错误,操作系统为什么会一直发送8号信号呢???
在cpu中会有一个eflag寄存器来记录是否溢出,和是否出现异常,当出现异常时操作系统作为硬件的管理者,就会对目标进程发送信号,由于我们捕捉了8号信号,会导致进程不能退出,而由于寄存器只有一套,当时间片到的时候会出现进程切换,此时就会对进程的上下文进行保存,当再次调度这个进程时就会再次发这个信号,就会造成上面的情况。
2.5.2 野指针的非法访问
和除0一样,代码如下:
cpp
void handler(int sig)
{
std::cout<<"hello, my pid :"<<getpid()<<" sig: "<<sig<<std::endl;
sleep(1);
}
int main()
{
signal(11,handler);
int *p=nullptr;
*p=1;
while(true)
{
std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
}
return 0;
}
运行结果如下:
出错和除0相似,不过是由于虚拟地址出错,就会向进程发送11号信号。
三.Core和Term
3.1 Core和Term区别
当时Term时,出现异常,进程就会异常终止,但是是Core时,出现异常,进程会异常终止,而且会给 我们生成一个debug文件,这个debug文件就是进程退出时的镜像文件,我们称为**核心转储**。那我们出现异常时怎么没有见到过核心转储?这是由于核心转储一般都是默认关闭的,如何查看呢?
cpp
ulimit -a
3.2 Core如何设置
其中第一个就是,如何打开呢?先设置一下输入指令:
ulimit -c 10240
然后运行一下有除0错误的代码,运行:
此时可以看到一个core文件
利用gdb进行调试
3.3 进程退出遗留问题
低7位是退出信号,次低8位是退出码,这个第8位就是我们是否进行了核心转储。
四.信号的保存
4.1 基本性质
- 实际执行信号的处理动作称为信号的递达(Deliverv)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞(Block)某个信号
- 发送信号后,被阻塞的信号时刻保存在未决的状态,直到进程解除对信号的阻塞,才能执行递达的动作
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,忽略是信号递达后的一种状态
信号在内核中右这样一个图:
其中block就是信号是否阻塞,它是一个位图结构,阻塞是1,没有阻塞是0,pending表表示信号是否出处于未决状态,同样是一个位图结构,handler表是表示信号递达后的动作,这是一个函数指针数组。
4.2 sigset_t和它的操作函数
sigset_t是操作系统提供的位图。sigset_t的操作函数有:
cpp
int sigemptyset(sigset_t* set);//将set的位图全部置为0
int sigfillset(sigset_t* set);//将set的位图全部置为1
int sigaddset(sigset_t* set, int signo);//将set的signo位置为0
int sigdelset(sigset_t* set, int signo);//将set的signo位置为0
int sigismember(const sigset_t* set, int signo);//将set的signo位置返回
4.3 sigprocmask
cpp
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
它的第一个参数是:
第二个参数是操作数,第三个参数是原来的sigset_t。这个函数是对block表进行修改。
4.4sigpending
cpp
int sigpending(sigset_t *set);
这个函数是将当前进程的pending表返回。
4.5 应用
根据上面的几个函数,那么我们就可以先将2号信号进行阻塞,然后发送2号信号就可以看到pending表由0变1,然后取消阻塞pending表由1变0(需要对2号进行捕捉)。下面看代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout<<"sig: "<<sig<<std::endl;
}
void Print( sigset_t s)
{
std::cout<<"my pid is: "<<getpid()<<" pending is: ";
for(int i=31;i>=0;i--)
{
if(sigismember(&s,i))
{
std::cout<<"1";
}
else
{
std::cout<<"0";
}
}
std::cout<<std::endl;
}
int main()
{
signal(2,handler);//捕2号信号
sigset_t set,oldset;
sigemptyset(&set);
sigemptyset(&oldset);
sigaddset(&set,2);
//修改当前进程的block表
sigprocmask(SIG_BLOCK,&set,&oldset);
int cur=20;
while(cur--)
{
sigset_t s;
sigpending(&s);
Print(s);
sleep(1);
if(cur==5)
{
sigprocmask(SIG_SETMASK,&oldset,&set);
}
}
return 0;
}
运行结果如图:
可以发现输入多少次2号信号都是处于阻塞,看到了从0到1再到0的过程。
五.信号的处理
5.1信号的捕捉
信号递达有三种处理方式:默认(signal(signo, SIG_DFL)),忽略 (signal(signo, SIG_IGN)),自定义捕捉(signal(signo,handler));信号肯恩那个不会立即处理而是选择在合适的时候,这个合适的时候就是进程从内核态返回到用户态的时候
信号产生后看它是怎末样子的?就先看它的pending表,如果时0就没有产生,如果是1就看它的block表看他是不是被阻塞,是1就是被阻塞无法继续,是0就看它的handler表,其中忽略和默认很好理解,但是自定义捕捉的方法是用户自己写的啊,操作系统不相信任何用户,既然是用户写的操作系统当然不会相信,防止这个做出超越自己权限的操作,所以需要内核态到用户态的转换。
上面图片还可以入如下理解(信号的捕捉需要4次切换)
5.2再谈地址空间
在地址空间中前面谈的都是0-3的地址空间,3-4是操作系统的,里面有操作系统的系统调用,它也有对应的页表,这意味着操作系统也在我们的地址空间中,所以我们无论如何进行进程切换都可以找到操作系统,我们访问操作系统其实还是在我们的地址空间中进行的,和我们访问库函数没有区别,由于操作系统不相信任何用户,所以需要访问3-4地址空间时需要一些约束。而且这个内核级页表只有一份!!
5.3键盘输入数据的过程
根据冯诺依曼体系可以直到键盘和cpu不能直接相连。但是键盘可以给cpu发送硬件中断,通过寄存器接受,例内存被分成一块一块的,里面都是函数指针,假如读键盘操作对应的下标是4,寄存器接受到的硬件中断就是4,然后调用读键盘的操作。
5.4如捕捉信号
cpp
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signal在前面谈论过了,这里不解释了。struct sigaction结构体的内容是:
cpp
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
今天只使用void (*sa_handler)(int), sigset_t sa_mask, int sa_flags;这三个参数,第一个是一个函数指针和signal的handler一样,第二个参数是sigset_t类型,在这里设为0,sa_flags设为0.其余操作和signal一样。
六.可重入函数
在insert时会有一个信号,这个信号也有一个insert,这样会造成node2的丢失,insert函数被执行流执行也被信号捕捉流执行,这个现象被称为被重入,函数称为不可重入函数。
七.volatile
看代码:
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
int flag=0;
int num=0;
void handler(int sig)
{
std::cout<<"get a sig: "<<sig<<" flag 0->1"<<std::endl;
flag=1;
}
int main()
{
signal(2,handler);
while(!flag);
std::cout<<flag<<std::endl;
return 0;
}
输入
cpp
g++ main.cc -o testsignal -O1
运行结果:
可以看到既然发送了2号信号,那么代码应该会可以结束,但是它一直运行,这就是因为代码优化的问题.
解决就是在flag前加上volatile关键字:
八.SIGCHLD
在父子进程中,父进程需要等待子进程,这是为了防止子进程出现僵尸问题,当子进程退出时会给父进程发送SIGCHLD信号,下面有两种情况:
8.1 当我们有多个子进程时,这些进程同时退出
当同一种信号产生时,如果这个信号正处于未决状态时,再次受到这个信号他都不会再次做出反应,所以多个子进程同时退出时会出现问题。所以我们可以采用下面解决方案:
cpp
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 chaild success done"<<std::endkl
break;
}
}
采用循环的方式当没有子进程时就结束,
8.2 多个子进程部分退
当只有部分退时采用上面显然有些浪费,在Linux中对SIGCHLD信号忽略就不用对子进程回收,操作系统会自动回收。所以只需要加入
cpp
signal(SIGCHLD,SIG_IGN);
存中...(img-GbpnIXT6-1723811396570)]
八.SIGCHLD
在父子进程中,父进程需要等待子进程,这是为了防止子进程出现僵尸问题,当子进程退出时会给父进程发送SIGCHLD信号,下面有两种情况:
8.1 当我们有多个子进程时,这些进程同时退出
当同一种信号产生时,如果这个信号正处于未决状态时,再次受到这个信号他都不会再次做出反应,所以多个子进程同时退出时会出现问题。所以我们可以采用下面解决方案:
cpp
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 chaild success done"<<std::endkl
break;
}
}
采用循环的方式当没有子进程时就结束,
8.2 多个子进程部分退
当只有部分退时采用上面显然有些浪费,在Linux中对SIGCHLD信号忽略就不用对子进程回收,操作系统会自动回收。所以只需要加入
cpp
signal(SIGCHLD,SIG_IGN);
就不需要对子进程进行等待,这种情况适用于不需要子进程退出信息的情况。