文章目录
信号
基本概念:
信号(signal)机制是UNIX系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。信号可以由各种异步事件产生,例如键盘中断等。Shell也可以使用信号将作业控制命令传递给它的子进程。信号是一种软件中断,提供了一种处理异步事件的方法,也是进程间通信的唯一一个异步的通信。
信号定义:
Linux系统中定义了一系列的信号,这些信号可以由内核产生,也可以由系统中的其他进程产生,只要这些进程有足够的权限。可以使用kill命令(kill-1)在机器上列出所有的信号,目前Linux 中定义了64中信号,前期定了32种(1-31),后面的33种为实时信号(32-64,一般如下所示。
进程可以屏蔽掉大多数的信号,除了SIGSTOP和SIGKILL。SIGKILL 和SIGSTOP 是不能被捕捉、阻塞、忽略 。SIGSTOP信号使一个正在运行的进程暂停,而信号SIGKILL则使正在运行的进程退出。进程可以选择系统的默认方式处理信号,也可以选择自己的方式处理产生的信号。信号之间不存在相对的优先权,系统也无法处理同时产生的多个同种的信号,也就是说,进程不能分辨它收到的是1个或 者是42个SIGCONT信号。
信号 | 产生原因 | 默认处理方式 |
---|---|---|
SIGABRT | 调用abort()函数时产生此信号。进程异常终止 | 终止进程,并且产生core dump 文件 |
SIGALRM | 超过用alarm()函数设置的时间时产生此信号 | 终止进程 |
SIGBUS | 总线错误(内存访问错误) | 终止进程,并且产生core dump 文件 |
SIGCHLD | 在一个进程终止或停止时, SIGCHLD信号被送给其父进程。如果希望从父进程中了解其子进程的状态改变,则应捕捉此信号。信号捕捉函数中通常要调用wait()函数以取得子进程ID和其终止状态 | 忽略信号 |
SIGCONT | 此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信号的进程处于停止状态,则操作系统的默认动作是使该停止的进程继续运行,否则默认动作是忽略此信号 | 如果进程处于stop状态,继续运行进程 |
SIGEMT | 指示一个实现定义的硬件故障 | |
SIGFPE | 此信号表示一个算术运算异常,例如除以0,浮点溢出等。 | 终止进程,并且产生core dump 文件 |
SIGHUP | 如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的进程 | 终止进程 |
SIGILL | 此信号指示进程已执行一条非法硬件指令 | 终止进程,并且产生core dump 文件 |
SIGINT | 当用户按中断键(一般采用Delete或Ctrl+C)时,终端驱动程序产生这个信号并将信号送给前台进程组中的每一个进程。当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出时,常用此信号终止它 | 终止进程 |
SIGIO | 此信号指示一个异步IO事件 | 终止进程 |
SIGPIPE | 如果在读进程已终止时写管道,则产生此信号 | 终止进程 |
SIGQUIT | 当用户在终端上按退出键(一般采用Ctrl+C)时,产生此信号,并送至前台进程组中的所有进程 | 终止进程,并且产生core dump 文件 |
SIGSEGV | 指示进程进行了一次无效的存储访问 | 终止进程,并且产生core dump 文件 |
SIGSTOP | 这是一个作业控制信号,它停止一个进程 | 停止进程 |
SIGSYS | 指示一个无效的系统调用。由于某种未知原因,某个进程执行了一条系统调用命令,但是调用命令所用的参数无效 | |
SIGTERM | 这是由kill命令发送的系统默认终止信号 | 终止进程 |
SIGTRAP | 指示一个实现定义的硬件故障,跟踪/断点陷阱 | 终止进程,并且产生core dump 文件 |
SIGTSTP | 交互停止信号,当用户在终端上按挂起键(一般采用Ctrl+Z)时,终端驱动程序产生此信号 | 停止进程 |
SIGTTIN | 当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号 | 停止进程 |
SIGTTOU | 当一个后台进程组进程试图写其控制终端时产生此信号 | 停止进程 |
SIGURG | 此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特率的数据时,此信号可选择地产生。 | 忽略信号 |
信号产生:
硬件方式
当用户按某些终端键时,引发终端产生的信号,例如Ctrl +C 通常会产生终端信号SIGINT
硬件异常产生信号。除数为0、无效的内存引用等等,这些通常由硬件检测到,并将通知内核。然后内核会为正在运行的进程产生适当的信号。例如对执行一个无效内存引用产生SIGSEGV信号;
软件方式
kill函数
将信号sig 发送给pid 进程;
#include <signal.h>
int kill (pid_t pid, int sig);
/*参数pid:
pid == 0,则发送sig 给调用该函数所属group 里所有的进程;
pid > 0,将信号sig 发送给进程ID 为pid 的进程;
pid < 0,将信号发送给其他进程组ID 等于pid 的绝对值;
pic == -1,将sig发送给发送进程有权限向它发送信号的系统上的所有进程;
返回值:
如果失败,返回-1;
如果成功,返回0;
失败原因:
给定的信号无效(errno = EINVAL)
发送权限不够( errno = EPERM )
目标进程不存在( errno = ESRCH )
*/
alarm函数
alarm函数可以用来设置定时器,定时器超时将产生SIGALRM信号给调用进程。
参数seconds表示设定的秒数,经过seconds后,内核将给调用该函数的进程发送SIGALRM信号。
如果seconds为0,则不再发送SIGALRM信号,最新一次调用alarm函数将取消之前一次的设定;
如果seconds 不为0,而上一次调用alarm还没有超时,那么将上次alarm 的余留值返回,并用新的alarm 代替上一次alarm;
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
信号处理流程
对于产生的信号有三种方式处理:
1、忽略此信号
大多数信号都可以用这种方式处理,但有两个信号决不能被忽略,SIGKILL 和SIGSTOP,这两个信号向超级用户提供了使进程终止或停止的可靠方法。
2、捕捉信号
可以通过函数将捕捉的函数通知给内核,在收到信号时,如果用户有捕捉的处理,内核会通知捕捉函数处理,处理完成后返回到内核中。下面会详细说明捕捉信号的机制。
3、执行系统默认的方式
捕捉信号处理流程(用户内核切换):
由于信号处理函数的代码是在用户空间的,设计内核,用户的交互、处理过程比较复杂,
举例说明:
1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了
内核信号处理
内核中每个进程都会对应 3 张表,每个信号都有3中状态:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示信号的处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
pending表:用4个字节的位图表示,位图的位置表示信号编号,内容表示是否pending。
block表:用4个字节的位图表示,位图的位置表示信号编号,内容表示是否block。
handler表:是一个句柄函数指针,数组即可表示,下标表示信号编号,内容表示信号处理的动作,为NULL表示没有处理该信号。
信号递达(Delivery):实际执行信号处理的动作。
信号未决(Pending):信号从产生到递达之间的状态。
信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
阻塞 :阻止信号被处理,而不是阻止产生。让系统暂时保留信号留待以后发送。
未决:信号递达前,信号产生到被处理
未决信号集存储了内核里所有没被处理的信号,标志为1证明未决。在处理前需要和阻塞信号集比较,看阻塞信号集对应位置是否为1(由用户调用系统API设置),如果是,则信号继续未决,知道阻塞解除,否则,信号被处理。只能标记一次,不能排队。
信号集和信号处理函数
信号集:多个信号可使用一个叫做信号集的数据结构表示 sigset_t.
信号处理函数
int sigemptyset(sigset_t*set) /*清空信号集,使所有标志位置0.*/
int sigfillset(sigset_t * set) /*清空信号集,所有标志位置1.*/
int sigaddset(sigset_t*set,int signum) /*将信号集某个信号置1,表示阻塞这个信号*/
int sigdelset(sigset_t*set,int signum) /*将信号集某个信号置0,表示不阻塞这个信号*/
int sigismember(const sigset_t*set,int signum)/* 判断某个信号是否阻塞(有效信号)。*/
int sigpromask(int how,const sigset_t*set,sigset_t *oldset)
/*
oset:如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
set:如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
how:
SIG_BLOCK 0 Block signals.
SIG_UNBLOCK 1 Unblock signals.
SIG_SETMASK 2 Set the set of blocked signals.
*/
int sigpending(sigset_t *set); /*获取内核的未决信号集,存储到set里*/
捕捉信号函数
signal()函数用于截取系统的信号,对此信号挂接用户自己的处理函数。其原型如下:
signal()函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第1个参数signum是一个整型数,第2个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述就是要向信号处理程序传送个整型参数,而它却无返回值。当调用signal设置信号处理程序时,第2个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值指向以前信号处理程序的指针。
#include<signal.h>
typedef void (*sighandler_t)(int); /*函数指针 参数为int 返回值为void*/
sighandler_t signal(int signum, sighandler_t handler);
第二个参数为:
SIG_IGN,内核忽略该信号,除了两个不能忽略;
SIG_DFL,采用系统默认方式处理该信号;
函数指针,按照用户的方式处理该信号;
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
如果出错,返回SIG_ERR;
如果成功,返回处理程序的指针;
信号量
#define SIGHUP 1 //hang up 挂断控制终端或进程
#define SIGINT 2 //interrupt 来自键盘的中断
#define SIGQUIE 3 //quit 退出
#define SIGILL 4 //illeagle 非法指令
#define SIGTRAP 5 //trap 跟踪断点
#define SIGABORT 6 //Abort 异常结束
#define SIGIOT 6 //IO trap 异常
#define SIGUNUSED 7 //Unused 没有使用
#define SIGFPE 8 //FPE 协处理器出错
#define SIGKILL 9 //kill 强迫终止
#define SIGUSR1 10 //use1 用户信号1,进程可使用
#define SIGSEGV 11 //segment violation 无效内存引用
#define SIGUSR2 12 //user2 用户信号2,进程可使用
#define SIGPIPE 13 //pipe 管道写出错
#define SIGALRM 14 //alarm 实时定时器报警
#define SIGTERM 15 //terminate 进程终止
#define SIGSTKFLT 16 //stack Fault 栈出错
#define SIGCHLD 17 //child 子进程停止
#define SIGCONT 18 //continue 回复进程继续
#define SIGSTOP 19 //stop 停止进程执行
#define SIGTSTP 20 //tty stop tty发出停止进程
#define SIGTTIN 21 //tty in 后台进程请求输入
#define SIGTTOU 22 //tty out 后台进程请求输出
#define SA_NOCLDSTOP 1 //进程处于停止状态,就不对sigchild进行处理
#define SA_NOMASK 0X40000000
//不阻止在指定的信号处理程序中再收到该信号
#define SA_ONESHOT 0X80000000
//信号句柄一旦被调用就恢复到默认处理句柄
#define SIG_BLOCK //在阻塞信号集中加上给定的信号集
#define SIG_UNBLOCK//从阻塞信号集中删除指定的信号集
#define SIG_SETMASK//设置阻塞信号集(信号屏蔽码)
#define SIG_DFL ((void(*)(int))0) //默认信号处理程序
#define SIG_IGN ((void (*)(int))1)//忽略信号处理程序
sigaction函数
sigaction函数可以读取和修改与指定信号相关联的处理动作。
/*成功返回0,失败返回-1*/
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数:
signo是指定信号的编号;
若act指针非空,则根据act修改该信号的处理动作。
若oact指针非空,则通过oact传出该信号原来的处理动作。
sigaction的结构体:
struct sigaction {
union {
__sighandler_t sa_handler; //可以是常数SIG_DFL或者SIG_IGN,或者是一个信号处理函数名;
void (*sa_sigaction) (int, siginfo_t *, void *);//与sa_handler是互斥的,两者通过sa_flags 确定选择哪个捕捉函数;
};
sigset_t sa_mask;//进程的信号屏蔽字。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字;
int sa_flags;//0或者SA_SIGINFO来指定使用sa_handler 还是使用sa_sigaction 函数;
void (*sa_restorer) (void);//保留,已过时;
};
例子
测试SIGINT信号
测试SIGINT信号, 对其进程阻塞,查看peding表,该信号是否发出,之后解除阻塞,进程终止
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void printfPending(sigset_t *pending)
{
int i=1;
for(;i<32;i++)
{
if(sigismember(pending,i))
{
printf("1");
}
else
printf("0");
}
printf("\n");
}
int main()
{
sigset_t set,oset; //定义两个信号集
sigemptyset(&set); //初始化信号集, 清空信号集,使所有标志位置0
sigemptyset(&oset);
sigaddset(&set,2); //将要操作的信号编号加入set信号集 2代表SIGINT(crtl + c 发出信号)
sigprocmask(SIG_SETMASK,&set,&oset);//将当前的set设置为信号屏蔽字,并将之前的内核信号屏蔽字保存到oset
sigset_t pending; //用于保存系统的pending表
int count=0;
while(1)
{
sigemptyset(&pending); //初始化,使所有标志位置0
sigpending(&pending); //拿到系统中的pending表保存到pending中
printfPending(&pending); //打印系统的pending表
sleep(2);
count++;
if(count>=5) //打印5次
{
sigprocmask(SIG_SETMASK,&oset,NULL);//将之前的信号屏蔽字重新设为block表,2号信号不会被阻塞,递达给系统,进程被终止。
}
}
return 0;
}
Makefile
all:demo
pipe_test: demo.o
gcc -o demo demo.o
clean:
rm -rf demo *.o
运行截图
打印5次之后,解除了对SIGINT信号的阻塞,进程终止
解决僵尸进程的例子
使用tcp socket建立服务器和客户端, 服务器主进程循环检测是否有新的客户端加入、如果存在新的客户端,fork子进程对客户端进行一对一处理,客户端发送数据完毕后自行退出, 对应的子进程调用exit(0),此时服务器主进程由于一直循环检测新的客户端进程,并没有对退出的子进程进行资源回收,导致子进程变为僵尸进程。
解决办法: 在服务器主进程中注册信号处理函数,收到SIGCHLD信号时,执行相应的信号处理函数,信号处理函数使用wait(NULL)等候子进程结束,对其资源回收,解决僵尸进程问题。
代码博客地址:https://blog.csdn.net/Hbw_aini/article/details/125361075?spm=1001.2014.3001.5502
参考博客
https://blog.csdn.net/m0_52012656/article/details/124611892
https://blog.csdn.net/shift_wwx/article/details/102549090?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-102549090-blog-123817818.pc_relevant_aa&spm=1001.2101.3001.4242.1&utm_relevant_index=1