目录
一、信号捕捉函数
1、signal函数
2、sigaction函数
二、用户态与内核态
1、用户态
2、内核态
用户态与内核态转换
三、volatile关键字
四、SIGCHLD信号
一、信号捕捉函数
1、signal函数
signal
函数是C语言标准库中的一个函数,用于处理Unix/Linux系统中的信号。信号是操作系统用于通知进程发生了某个事件的一种机制,比如用户按下Ctrl+C时发送的SIGINT信号,或者某些错误条件如除零错误产生的SIGFPE信号。signal
函数允许程序定义对这些信号的响应方式,而不是采用默认行为(通常是终止进程)。
函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum
:要处理的信号的编号,例如SIGINT(2号中断信号,通常是Ctrl+C),以及其它信号。handler
:处理函数的地址。这可以是以下几种形式:
SIG_DFL
:恢复该信号的默认处理行为(通常是终止进程)。SIG_IGN
:忽略该信号。- 自定义函数:一个用户定义的函数指针,当信号发生时将调用该函数。该函数原型应为
void function_name(int signum)
,其中signum
是接收到的信号编号。
返回值:
signal
函数返回与信号关联的先前处理函数的指针。如果之前没有安装处理函数,则返回值可能是SIG_ERR
(表示发生错误)或者在某些实现中是默认的处理行为。
这个函数在前面文章也使用过很多次,前面也对一些常用信号进行了说明,可以参考一下这篇文章:Linux:进程信号(一)信号的产生
2、sigaction函数
sigaction
函数是POSIX标准中用于管理信号的高级接口,相比signal
函数提供了更强大和灵活的信号处理能力。它允许程序不仅指定信号处理函数,还能控制信号处理时的其他方面,如信号掩码和额外的信号处理选项。
函数原型:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明:
signum
:要操作的信号编号。act
:指向一个struct sigaction
结构体的指针,用于设置新的信号处理行为。oldact
:指向另一个struct sigaction
结构体的指针,用于存储以前的信号处理行为(如果不关心旧的行为,可以传入NULL)。
sigaction结构体:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数指针,兼容老版本
void (*sa_sigaction)(int, siginfo_t *, void *); // 新的、更强大的信号处理函数指针
sigset_t sa_mask; // 处理该信号时需要阻塞的其他信号集合
int sa_flags; // 信号处理的标志,如SA_RESTART, SA_NODEFER等
// 可能还有其他平台相关的字段
};
信号处理函数:可以通过
sa_handler
或sa_sigaction
指定。如果使用sa_sigaction
,则可以访问到更多关于信号的信息,如发送信号的进程ID和信号的具体原因。信号掩码(
sa_mask
):在信号处理函数执行期间,指定的信号将被临时阻塞,以避免递归或干扰信号处理过程。标志(
sa_flags
):控制信号处理的额外行为,例如SA_RESTART
可以让某些系统调用在被信号中断后自动重启,而SA_NODEFER
可以防止在处理该信号时该信号被阻塞。
我们使用以下代码来测试一下:
#include<signal.h>
#include<iostream>
#include <sys/types.h>
#include <unistd.h>
void printsigset(sigset_t *set)
{
for(int i=31;i>=0;i--)
{
if(sigismember(set,i))
{
std::cout<<"1";
}
else
{
std::cout<<"0";
}
}
std::cout<<std::endl;
}
void handler(int signo)
{
std::cout << "get a sig: " << signo << std::endl;
sleep(20);
}
int main()
{
// sigset_t set,oset;
// sigemptyset(&set);
// sigemptyset(&oset);
// sigaddset(&set,SIGINT);//屏蔽2号信号
// sigaddset(&set,3);//屏蔽3号信号
// sigaddset(&set,4);//屏蔽4号信号
// sigaddset(&set,5);//屏蔽5号信号
// sigprocmask(SIG_BLOCK,&set,&oset);
struct sigaction act, oact;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaction(2,&act,&oact);
std::cout<<"pid"<<getpid()<<std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
printsigset(&pending);
sleep(1);
}
return 0;
}
运行可以看到:
先发出2号信号,这个时候向它发出三号信号时会被阻塞,并不会执行,因为在前面添加了三号信号的阻塞,当2号信号对应的handler结束时,才会被处理。
如果在未处理2号信号时,直接发送3号信号,那么会直接终止:
如果有处理函数(通过上述方式注册),内核会执行以下操作:
保存现场:保存当前程序上下文,包括寄存器状态等,以便稍后恢复执行。
修改信号掩码:通常,在执行信号处理函数之前,内核会临时修改进程的信号掩码,以防止在处理信号过程中被相同的或其他信号打断,除非这些信号被特别设置为不阻塞。
调用信号处理函数:执行用户定义的信号处理函数。在此期间,进程处于用户态。
恢复现场:信号处理完毕后,内核恢复之前保存的程序上下文,进程从被中断的地方继续执行,或者根据信号处理函数的返回情况和系统调用的重启规则决定后续动作。
二、用户态与内核态
用户态(User Mode)和内核态(Kernel Mode),也称为用户空间和内核空间,是现代操作系统中的两种处理器执行模式,它们定义了程序运行时的不同权限级别和访问能力。
1、用户态
在用户态下,程序(通常是应用程序)只能执行非特权指令,不能直接访问硬件资源或执行对系统稳定性有风险的操作。这意味着程序不能直接读写内存、操作外设、更改系统时间等。
应用程序的大部分时间都在用户态下运行,这样可以保证系统的安全性和稳定性,因为程序错误不会直接影响到操作系统的核心部分。
如果用户态程序需要执行如磁盘I/O、网络通信等操作,它必须通过系统调用(System Call)向操作系统请求服务,这时就会从用户态转换到内核态。
2、内核态
内核态下,程序可以执行所有指令,包括特权指令,可以直接访问和控制所有系统资源,如内存、I/O设备等。操作系统内核及其相关模块(如设备驱动程序)在内核态下运行,负责管理系统资源、处理中断、调度进程等核心任务。
当系统调用发生时,CPU从用户态切换到内核态,操作系统内核执行所需的服务,并在完成后返回用户态。这一过程涉及到堆栈的切换、权限级别的变化和上下文的保存与恢复。
内核态还负责处理硬件中断和异常,无论当前处理器处于何种状态,一旦中断或异常发生,CPU都会立即进入内核态以处理这些事件。
用户态与内核态转换
转换通常由硬件支持,并由操作系统控制。从用户态到内核态的转换通常是通过执行特定指令(如系统调用指令)、产生中断或异常来触发。
从内核态返回用户态通常发生在系统调用完成、中断处理结束或异常处理完成后,操作系统通过执行相应的返回指令来实现状态的恢复。
这种区分和转换机制是操作系统设计的基础之一,旨在提供隔离和保护,确保系统稳定性和安全性,同时允许用户程序有效利用系统资源。
三、volatile关键字
volatile
关键字在C/C++等编程语言中是一个类型修饰符,用于指示编译器不要对被修饰的变量进行任何优化假设,确保程序的执行符合预期,特别是在多线程编程和硬件交互场景中。
四、SIGCHLD信号
wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
僵尸进程:
运行以下代码:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include<sys/types.h>
#include<unistd.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
waitpid(-1, NULL, WNOHANG);
return 0;
}
不产生僵尸进程还有另外一种办法 : 父进程调用 sigaction 将 SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略通常是没有区别的, 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可用。