进程信号
- 进程信号
- 什么是信号
- liunx信号种类
- 前后台进程
- 前后台进程的概念
- 进程信号的产生
- 键盘产生
- 阻塞信号
- 信号的捕捉
- 用户态和内核态
- 信号的捕捉函数
进程信号
什么是信号
信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。 |
liunx信号种类
Liunx中如何查看信号种类:
信号名称 | 信号编号 | 描述 |
---|---|---|
SIGHUP | 1 | 终端挂起或控制进程结束 |
SIGINT | 2 | 终止进程(由键盘输入 Ctrl+C 产生) |
SIGQUIT | 3 | 终止进程并生成核心转储文件(由键盘输入 Ctrl+\ 产生) |
SIGILL | 4 | 非法指令 |
SIGTRAP | 5 | 跟踪陷阱 |
SIGABRT | 6 | 终止进程并生成核心转储文件(由调用 abort() 函数产生) |
SIGBUS | 7 | 非法地址访问 |
SIGFPE | 8 | 浮点异常 |
SIGKILL | 9 | 无条件终止进程 |
SIGUSR1 | 10 | 用户定义的信号1 |
SIGSEGV | 11 | 无效的内存引用 |
SIGUSR2 | 12 | 用户定义的信号2 |
SIGPIPE | 13 | 管道破裂 |
SIGALRM | 14 | 定时器超时 |
SIGTERM | 15 | 请求终止进程(默认终止信号) |
SIGSTKFLT | 16 | 协处理器栈错误 |
SIGCHLD | 17 | 子进程状态改变 |
SIGCONT | 18 | 继续被暂停的进程 |
SIGSTOP | 19 | 停止进程 |
SIGTSTP | 20 | 暂停进程(由键盘输入 Ctrl+Z 产生) |
SIGTTIN | 21 | 后台进程尝试读取控制终端 |
SIGTTOU | 22 | 后台进程尝试写控制终端 |
SIGURG | 23 | 套接字的紧急情况 |
SIGXCPU | 24 | CPU 时间限制超时 |
SIGXFSZ | 25 | 文件大小限制超过 |
SIGVTALRM | 26 | 虚拟定时器超时 |
SIGPROF | 27 | 分析计时器超时 |
SIGWINCH | 28 | 窗口大小调整 |
SIGIO | 29 | 异步 I/O 事件 |
SIGPWR | 30 | 电源故障 |
SIGSYS | 31 | 非法系统调用 |
前后台进程
前后台进程的概念
前台进程在命令行操作时,只能有一个,后台进程可以有多个。
接下来我们启动一个后台进程。
我们创建一个后台进程,并且让他给文件不停的写入。
此时我们查看 log.txt
可以看到。进程在后台运行,那我们如何查看呢?
jobs 就可以看到
如何杀掉后台进程呢? 我们需要把fg后台放到前台,然后退出。
下面附上常用命令:
快捷键 | 描述 |
---|---|
Ctrl+C | 终止并退出前台进程,回到Shell |
Ctrl+Z | 暂停前台命令执行,放到后台,回到Shell |
jobs | 查看当前在后台执行的命令 |
& | 在后台执行命令 |
fg N | 将进程号码为N的命令放到前台执行 |
bg N | 将进程号码为N的命令放到后台执行 |
进程信号的产生
键盘产生
用户在Linux下执行一个前台进程,然后使用ctrl+c操作,会直接终止掉该前台进程。这是因为用户按下ctrl+c,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
ctrl+c组合键只能终止掉前台进程,对后台进程无效。这时我们可以使用kill -9指令来该终止进程
crtl+c的信号本质就是2号信合
接下来我们通过一个函数验证这个事:
像进程发送2号信合,运行新程序。
查看进程退出码:echo $?
························································································································································
阻塞信号
信号的常见分类:
- 实际执行信号的处理动作,称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(pending)。
- 进程可以选择阻塞(Block)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。
在内核中的表示
未决信号,就是你的进程已经接收到了信号了,只是还没被信号处理函数处理的那些信号。
特别说明:虽然未决信号的定义是上面这样,但是这里我们需要更加具体一点,未决信号特指进程收到且被阻塞的信号。
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
- 在上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- 在上图中,SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会在改变处理动作之后再接触阻塞。
- 在上图中,SIGQUIT信号未产生,正在被阻塞,不能执行自定义函数。
- 处理动作包括默认、忽略以及自定义。
sigset_t
根据信号在内核中的表示方法,每个信号的未决标志只有一个比特位,非0即1,如果不记录该信号产生了多少次,那么阻塞标志也只有一个比特位。
sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。 在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”,至于这个类型内部如何存储这些bit则依赖于系统的实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
#include <signal.h>
int sigemptyset(sigset_t *set);// 对指定信号集进行清空。
int sigfillset(sigset_t *set);//对指定信号集进行置1
int sigaddset(sigset_t *set, int signum);//将指定的信号signum添加到信号集中 也就是置1
int sigdelset(sigset_t *set, int signum);//将指定的
信号signum在信号集中置0
int sigismember(const sigset_t *set, int signum); //判定signum是否为1
接下来介绍重要函数:
#include<signal.h>
int sigprocmask(int how,const sigset_t *set,__sigset_t *oset);
//若返回成功则返回0.
how参数
选项 | 含义 |
---|---|
SIG_BLOCK | 设置包含了我们希望添加到当前信号屏蔽字的信号,相当于 `mask = mask |
SIG_UNBLOCK | 设置包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask = mask & ~set |
SIG_SETMASK | 设置当前信号屏蔽字为 set 所指向的值,相当于 mask = set |
接下来我们屏蔽2号信合!
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
cout<<"handler get signo:"<<signo<<endl;
}
int main()
{
signal(2,handler);
//set==block oset== oblock
sigset_t set,oset;
sigemptyset(&set);//初始化
sigemptyset(&oset);
sigaddset(&set,2);//设置对2号信合屏蔽 但是并没有设置进入系统
sigprocmask(SIG_BLOCK,&set,&oset);
while(1)
{
cout<<"mypid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
#############################
设置完成后,我们对代码进行编译。
可以发现我对2号信合免疫了。
pending未决队列表的获取:
sigpending
读取当前进程的未决信号集,通过set参数传出来。
接下来我们以一个综合案例操作:
- 屏蔽2号信合 这个屏蔽是阻塞
- 通过sigpending查询2号信合
- 发送2号信合。
我们可以看到,发送2号信合,此时2号信合处于未决状态。
在10次后,我们取消对2号的阻塞
信号的捕捉
信号的捕捉发生在用户态和内核态的互相转换中,首选我们必须先知道什么是用户态,什么是内核态。
用户态和内核态
①OS在不在内存中被加载呢? ?——在 无论进程怎么切换,我们都可以找到内核的代码和数据,前提是你只要能够有权利访问!
②当前进程如何具备权利 访问这个内核页表乃至访问内核数据呢?——要进行身份切换。
进程如果是用户态的——>只能访问用户级页表 0~3G
进程如果是内核态的——>访问内核级和用户级的页表 3~4G 、③我怎么知道我是用户态的还是内核态的呢?
CPU内部有对应的状态寄存器CR3, CR3有比特位标识当前进程的状态 0:内核态,3:用户态④0—>3
用户态切到内核态的情况:1.系统调用的时候。2.时间片到了,进程间切换。3.其他等等。执行完毕就继续切回用户态。即:程序运行从用户态切换到内核态的操作:中断/异常/系统调用,例如<1> 整数除以零操作会导致用户态—>内核态:因为会导致程序异常(分母不能为0) <2> sin()函数调用操作不会切换状态,因为库函数并不会引起运行态的切换 <3> read 系统调用操作会导致用户态—>内核态:符合系统调用接口
⑤内核态vs用户态 内核态可以访问所有的代码和数据——内核态具备更高权限 用户态只能访问自己的
⑥我们的程序,会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,你并没有自己去操作这些软硬件资源,而是必须通过OS-
无数次的陷入内核(1.切换身份3->0 2.切换页表,切到内核级页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份0->3 2. 切换页表,切到用户级页表)->用户得到结果
⑦while(1);死循环进程普通程序会身份切换吗? —>也会陷入内核,来回切换身份 —>你也有自己的时间片
—>时间片到了的时候->OS收到中断信息,把进程从cpu移走,进程切到内核态,更换内核级页表 —>保护上下文,执行调度算法
—>选择了新的进程 —>恢复新进程的上下文 —>用户态,更换成用户级页表 —>CPU执行的就是新进程的代码!
重点: 用户态到内核态的两种切换场景:
- 除0错误。操作系统互捕捉这个错误,并且切换到内核态
- 使用系统调用函数接口,会从用户态切换到内核态。
自定义捕捉信号的处理过程
这个图是对自定义信号的处理,以及检测时机。
- 调用系统调用函数,此时用户态切换内核态
- 此时操作系统处理完之后,检测处理信号
- 信号为自定义信号处理方式 ,内核态不处理用户态代码
- 处理完成,继续返回内核态
- 从内核态返回用户态
正无穷记忆法!!!
信号的捕捉函数
- signal 常用捕捉函数
sigaction
信号捕捉函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
该函数需要将方法写入结构体中。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
**sa_handler:**这是一个指向函数的指针,当接收到指定的信号时,这个函数会被调用。如果设置为 SIG_IGN,则忽略该信号;如果设置为 SIG_DFL,则使用默认的信号处理行为。
**sa_mask:**这是一个信号集,用于在信号处理函数执行期间阻塞的信号。这确保了在处理一个信号时,不会受到其他信号的干扰。 也就是说,阻塞二号信号的
其他的几个参数并不需要我们了解。