信号的概念:
在Linux操作系统中,信号是一种软件中断机制,用于通知进程某个事件已经发生。信号是Linux进程间通信(IPC)的一种简单且快速的方式,它可以用来处理各种异步事件,如用户输入、硬件事件或软件条件。
信号的特点:
进程可以选择阻塞某些信号,阻止这些信号的传递。
每个信号都有一个默认行为,例如终止进程、忽略或停止进程等
信号有不同的优先级,高优先级的信号会打断低优先级的信号处理。
信号可以跨越进程边界,由一个进程发送给另一个进程
信号机制是由操作系统内核实现的,与操作系统的调度和资源管理紧密相关。
信号的来源:
在Linux操作系统中,信号可以由多种来源产生,用于通知进程发生了某些重要事件。
-
硬件异常:
SIGSEGV
:尝试访问未分配的内存或无效的内存区域。SIGFPE
:发生浮点异常,如除以零。SIGILL
:执行了非法指令。
-
软件条件:
SIGINT
:通常由用户通过按下Ctrl+C产生,用于中断正在运行的程序。SIGTERM
:默认信号,用于请求程序自己终止。SIGALRM
:由alarm
函数设置的计时器到期时产生。
-
用户干预:
SIGKILL
:由kill
命令发送,用于立即终止程序,无法被捕获或忽略。SIGSTOP
:由kill
命令发送,用于停止程序的执行,无法被捕获、忽略或由用户生成。
-
系统调用:
SIGCHLD
:子进程结束时,父进程会收到此信号。SIGHUP
:当控制终端关闭时,如关闭或拔出调制解调器,相关进程会收到此信号。
-
资源限制:
SIGPIPE
:写入一个没有读进程的管道时产生。SIGXCPU
:超过CPU时间限制。SIGXFSZ
:超过文件大小限制。
-
I/O操作和设备状态变化:
SIGURG
:有紧急数据可从套接字读取时产生。SIGIO
:文件描述符上的I/O操作现在可进行时产生。
-
调度器事件:
- 某些实时调度器可能会在特定事件发生时发送信号。
-
外部设备或硬件设备:
- 特定硬件设备可能会在检测到特定事件时发送信号
信号的种类:
在Linux系统可以通过 kill -l 命令查看
常用信号及其编号:
SIGHUP
(1):挂起信号,通常在终端关闭时发送给前台进程组。
SIGINT
(2):中断信号,通常由用户通过按下Ctrl+C产生。
SIGILL
(4):非法指令信号,当进程执行非法指令时发送。
SIGABRT
(6):中止信号,由abort()
函数调用产生,用于异常终止进程。
SIGBUS
(7):总线错误信号,当硬件异常,如内存访问错误时发送。
SIGFPE
(8):浮点异常信号,如算术溢出、除以零等。
SIGKILL
(9):杀死信号,用于立即终止进程,无法被捕获或忽略。
SIGUSR1
(10):用户定义信号1,用途由用户自定义。
SIGSEGV
(11):段错误信号,当访问无效内存段时发送。
SIGUSR2
(12):用户定义信号2,用途由用户自定义。
SIGPIPE
(13):管道信号,当写入一个没有读进程的管道时发送。
SIGTERM
(15):终止信号,用于请求程序自己终止,可以被捕获或忽略。
SIGCHLD
(17):子进程结束信号,当子进程结束时发送给父进程。
SIGCONT
(18):继续信号,用于唤醒一个被停止的进程。
SIGSTOP
(19):停止信号,用于停止进程的执行,无法被捕获或忽略。
SIGTTIN
(21):后台进程试图读终端时发送。
SIGTTOU
(22):后台进程试图写终端时发送。
SIGIO
(29):I/O信号,当文件描述符上有可进行的I/O操作时发送。
信号处理的流程:
信号处理流程包含以下两个方面:
1. 信号的发送
信号的发送可以由以下几种方式触发:
- 用户操作:用户可以通过键盘产生信号,如按下Ctrl+C通常发送
SIGINT
(中断信号)。 - 软件生成:程序可以通过
kill
系统调用或raise
函数发送信号给其他进程或自身。 - 硬件异常:当程序执行非法操作(如除零、内存访问违规)时,硬件会触发信号,如
SIGFPE
(浮点异常)或SIGSEGV
(段错误)。 - 系统条件:系统在特定条件下也会发送信号,如
SIGHUP
(挂起信号)可能在控制终端关闭时发送。 - 定时器超时:使用
alarm
、setitimer
或timer
等定时器函数设置的定时器超时后,会发送如SIGALRM
或SIGVTALRM
信号。
2. 信号的投递与处理
信号的投递与处理涉及以下几个步骤:
- 信号队列:当一个信号被发送给进程时,它首先被放入进程的信号队列中。
- 信号屏蔽:进程可以通过设置信号掩码(使用
sigprocmask
函数)来阻止某些信号的投递。被屏蔽的信号不会立即投递,直到它们被进程从屏蔽列表中移除。 - 信号投递:内核负责将信号从队列中取出并投递给进程。如果信号未被屏蔽,内核会根据信号处理函数的设置来处理信号。
- 默认处理:如果进程没有为信号设置处理函数,或者信号被忽略(使用
signal
或sigaction
函数设置为SIG_IGN
),内核将执行信号的默认操作,如终止进程或忽略信号。 - 用户定义的处理:如果进程为信号定义了处理函数,内核在信号到达时调用该函数。处理函数可以执行任何清理或响应操作。
- 处理完成:信号处理函数执行完毕后,进程恢复到信号到达前的状态,继续执行。如果信号处理函数调用了如
exit
或_Exit
等函数,进程将终止。
在 Linux 中对信号的处理方式如下:
在内核中的用于管理进程的结构为 task_struct ,
1.忽略信号
即对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
SIGKILL
用于立即终止进程,而SIGSTOP
用于停止进程的执行。这两个信号是为了保证系统管理员能够控制所有进程而设计的。
2.捕捉信号
- 进程可以捕捉信号,并定义自己的信号处理函数。当信号发生时,如果进程为该信号注册了处理函数,内核会暂停进程的执行,转而执行该信号的处理函数。
- 信号处理函数可以是标准函数,如
signal()函数
signal()函数
函数头文件
#include <signal.h>
函数原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数
signum:指定要处理的信号的编号。例如,SIGINT(通常由Ctrl+C产生)。
handler:指向一个函数的指针,该函数将被调用以处理指定的信号。这个函数可以是程序自定义的,也可以是几个标准函数之一:
SIG_DFL:执行信号的默认操作。
SIG_IGN:忽略该信号
函数返回值
signal函数返回之前为该信号设置的处理函数的指针。如果之前没有设置处理函数,或者设置的是默认操作,它将返回SIG_DFL。如果之前设置的是忽略该信号,它将返回SIG_IGN。
如果signal函数调用失败,它将返回SIG_ERR,errno将被设置以指示错误原因
示例代码:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signal_handler(int sig) {
printf("Received signal %d\n", sig);
// 执行必要的动作
}
int main() {
// 设置SIGINT信号的处理函数
signal(SIGINT, signal_handler);
while(1) {
sleep(1);
printf("Looping...\n");
}
return 0;
}
3.执行默认操作
如果进程没有特别处理某个信号,Linux系统会为该信号执行默认操作。默认操作通常是终止进程、忽略信号或停止进程。
SIGTERM
(信号15):默认操作是终止进程。SIGCHLD
(信号17):默认操作是忽略,通常用于子进程结束时通知父进程。SIGCONT
(信号18):默认操作是继续执行之前被停止的进程
信号发送操作
在Linux中,kill()
和 raise()
函数是用于发送信号的两个常用方法。它们允许进程之间相互发送信号,或者进程可以向自己发送信号
1.kill()函数
kill()
函数可以向一个进程或进程组发送信号。这是最常用的信号发送方式,尤其是在需要向其他进程发送信号时。
函数头文件
#include <sys/types.h>
#include <signal.h>
函数原型
int kill(pid_t pid, int sig);
函数参数
pid:目标进程的进程ID(PID)。
如果pid为正,则信号信号被发送到具有pid指定的ID的进程。
如果pid等于0,则向调用进程的进程组中的每个进程发送sig。
如果pid等于-1,则sig将发送到调用进程有权发送信号的每个进程,进程1 (init)除外
如果pid小于-1,则向进程组中ID为-pid的每个进程发送sig
sig:要发送的信号。可以是任何有效的信号,如SIGINT、SIGTERM等。
函数返回值
成功 返回0。
失败 返回-1,并设置errno以指示错误原因。
2.raise()函数
函数头文件
#include <sys/types.h>
#include <signal.h>
函数原型
int raise(int sig);
函数参数
sig:要发送给调用进程的信号编号。这是一个整数,表示特定的信号,如SIGINT、SIGTERM等。
函数返回值
成功 返回0。
失败 返回-1,并设置errno以指示错误原因
等待信号操作
在Linux系统中,pause()
函数用于使调用它的进程挂起,直到它接收到一个信号。这是一个简单的阻塞调用,常用于进程初始化后等待异步信号的情况
pause()函数
函数头文件
#include <unistd.h>
函数原型
int pause(void);
函数描述
pause() 函数使调用它的进程挂起,直到它捕获到一个信号。该进程将一直停留在pause()调用处,直到有信号到达并被处理。
该函数没有参数。
它返回时,通常意味着一个信号已经被接收并处理。
函数返回值
pause() 在信号处理函数执行后返回。如果信号处理函数返回,pause() 将返回。
如果因错误而失败,它将返回-1,并设置errno以指示错误原因
总结:
示例代码:
下面程序展示了如何使用信号和fork
来创建和协调多个进程。父进程创建两个子进程,然后向它们发送不同的信号。第一个子进程等待并接收SIGUSR1
信号,第二个子进程等待并接收SIGUSR2
信号。子进程使用pause
挂起等待信号,接收到信号后继续执行并退出。父进程等待子进程结束后也退出。这个程序展示了信号在进程间通信中的应用,并且正确地处理了子进程的结束,使用了wait(NULL)
来避免子进程成为僵尸进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
typedef void (*sighandler_t)(int);
void sighandler(int signum)
{
printf("sighandle:%s\n",strsignal(signum));
}
int main()
{
sighandler_t sig = signal(SIGUSR1,sighandler);
if(sig==SIG_ERR)
{
perror("signal sig");
exit(EXIT_FAILURE);
}
sighandler_t sigt = signal(SIGUSR2,SIG_DFL);
if(sigt==SIG_ERR)
{
perror("signal sigt");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1)
{
perror("fork failed");
return 1;
}
else if(pid == 0)
{
printf("<1>chilb process pid :%d\n",getpid());
pause();
printf("<1>chilb process end\n");
exit(EXIT_SUCCESS);
}
else
{
pid_t Apid = fork();
if(Apid==-1)
{
perror("fork1:");
exit(EXIT_FAILURE);
}
else if(Apid==0)
{
printf("<2>chilb process Apid :%d\n",getpid());
pause();
printf("<2>chilb process end\n");
exit(EXIT_SUCCESS);
}
else
{
printf("parent process pid :%d\n",getpid());
sleep(1);
int result = kill(pid,SIGUSR1);
if(result==-1)
{
perror("kill pid");
exit(EXIT_FAILURE);
}
int ret = kill(Apid,SIGUSR2);
if(ret==-1)
{
perror("kill Apid");
exit(EXIT_FAILURE);
}
printf("parent process end\n");
}
}
wait(NULL);
return 0;
}
代码解读:
定义sighandler_t
类型,它是一个指向函数的指针,该函数接受一个整数参数(信号编号)并且没有返回值。
定义sighandler
函数,它是一个信号处理函数,当接收到信号时被调用。它使用strsignal
函数将信号编号转换为信号名称的字符串,并打印出来。
在main
函数中,首先设置SIGUSR1
信号的处理函数为sighandler
。如果设置失败,打印错误信息并退出。
将SIGUSR2
信号的处理函数设置为默认操作(SIG_DFL
)。如果设置失败,打印错误信息并退出。
创建第一个子进程。如果fork
失败,打印错误信息并返回1。
如果当前是子进程(pid == 0
),打印子进程的PID,然后调用pause
挂起等待信号。接收到信号后,打印结束信息并退出。
如果当前是父进程,再创建第二个子进程。如果fork
失败,打印错误信息并退出。
如果当前是第二个子进程(Apid == 0
),打印子进程的PID,然后调用pause
挂起等待信号。接收到信号后,打印结束信息并退出。
如果当前是父进程,等待1秒,然后向第一个子进程发送SIGUSR1
信号,向第二个子进程发送SIGUSR2
信号。如果发送信号失败,打印错误信息并退出。
父进程打印结束信息。
父进程调用wait(NULL)
等待任一子进程结束。
结语:
无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力