Linux —— 信号(2)
- 信号的序号
- kill系统调用
- 不可被自定义的信号
- raise
- abort
- 异常传递信号
- SIGFPE
- SIGSEGV
- alarm传递信号
我们今天来接着了解信号:
信号的序号
我们来看看信号的序号:
信号的序号是从1开始,到31我们称这区间的信号为普通信号,然后又从34到64为实时信号。
这里我们要明确一点,没有0号信号:
在Linux中,不存在编号为0的信号。实际上,信号编号是从1开始的,并没有0号信号。这是因为Linux内核和相关的POSIX标准在定义信号时,从1开始为各种信号分配了编号,而0号并没有被分配给任何信号。
此外,kill函数对信号编号0有特殊的应用。当你向一个进程发送信号0时,实际上并不会发送任何信号给该进程,而是用来测试进程是否存在以及当前用户是否有权限向该进程发送信号。如果进程存在并且用户有权限,那么kill函数会成功返回;否则,会返回一个错误码。
因此,Linux中没有0号信号是因为在信号编号的分配中,0并没有被分配给任何信号,而kill函数对信号编号0的特殊应用则是一种特殊的机制,用于测试进程的存在性和权限。
kill系统调用
我们除了可以命令行来向进程发送信号之外,我们还可以用kill来向进程发送信号:
在Linux中,kill系统调用(通常通过C语言库函数kill()来访问)用于向一个进程发送一个信号。这个系统调用允许一个进程向另一个进程发送一个信号,该信号可以是任何在系统中定义的信号。
以下是kill()函数的基本用法:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数说明:
pid
:要接收信号的进程的进程ID(PID)。如果pid小于-1,则信号将被发送到进程组ID等于-pid的所有进程。如果pid等于0,则信号将被发送到与调用进程属于同一进程组的所有进程。如果pid等于-1,则信号将被发送到除调用进程自身外的所有进程。
sig
:要发送的信号。这可以是任何在系统中定义的信号,如SIGTERM(终止进程)、SIGINT(中断进程)、SIGKILL(强制终止进程,不能被捕获或忽略)等。
返回值:
如果成功,返回0。
如果失败,返回-1,并设置errno以指示错误。
以下是一个简单的示例,展示了如何使用kill()函数来终止一个进程:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include<iostream>
static void Usage(const std::string& proc)
{
std::cout << "Usage: ";
std::cout << proc << "<pid>\n" <<std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(EXIT_FAILURE);
}
int signalnumber = std::atoi(argv[1] + 1);
pid_t pid = atoi(argv[2]);
if (kill(pid, signalnumber) == -1)
{
perror("kill");
exit(EXIT_FAILURE);
}
printf("Sent SIGTERM to process %d\n", pid);
exit(EXIT_SUCCESS);
}
我们重新写一段代码:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
int main()
{
while(true)
{
std::cout << "myprocess is running... pid:"<< getpid() << std::endl;
sleep(1);
}
return 0;
}
然后我们跑起来,用我们写的程序来传递信号:
在这个示例中,程序接受两个个命令行参数(要传递的信号,要终止的进程的PID),并使用kill()函数向该进程发送信号。如果发送信号失败,程序将打印一条错误消息并退出。如果成功,程序将打印一条消息表明信号已发送。
不可被自定义的信号
还记得我们可以自定义信号的行为吗:
void signal_hander(int signum)
{
printf("Caught SIGINT, %d\n",signum);
}
int main()
{
while(true)
{
cout << "process running ... pid: "<< getpid() << endl;
pid_t id = getpid();
signal(2,signal_hander);
kill(id,2);
sleep(1);
}
}
但是如果换成9号信号:
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include<cstdio>
using namespace std;
void signal_hander(int signum)
{
printf("Caught SIGINT, %d\n",signum);
}
int main()
{
while(true)
{
cout << "process running ... pid: "<< getpid() << endl;
pid_t id = getpid();
signal(9,signal_hander); //换成了九号信号
kill(id,9);
sleep(1);
}
}
然后我们发现我们的9号信号并没有按照我们自定义的行为来执行,这是因为,有一些信号是不能被自定义的:
在Linux系统中,并非所有信号都可以被程序自动捕捉并作出响应。有几个信号默认是不可被捕获的,也就是说,它们不能通过信号处理函数来处理,一旦这些信号被发送给进程,将导致默认的行为立即发生,无法通过编程来改变其后果。这些信号主要是:
- SIGKILL (信号9):此信号用于强制终止一个进程。操作系统保证无论进程处于何种状态,一旦收到这个信号,都会立即终止,且进程无法忽略或捕获这个信号。
- SIGSTOP (信号19):这个信号用于暂停(停止)一个进程的执行。同SIGKILL一样,进程无法忽略或捕获这个信号,一旦接收到就会立即停止运行。
这两个信号设计成不可被捕获,是为了确保系统有办法在任何时候控制和管理进程,即使进程本身出现问题或试图逃避正常的控制机制。这对于维护系统的稳定性和安全性至关重要。其他信号,如SIGINT (Ctrl+C触发, 信号2),SIGTERM (默认的终止信号, 信号15)等,则是可以被捕获并由程序自定义处理行为的。
raise
在Linux中,raise
函数允许进程向自身发送一个信号。这是一个方便的机制,特别是在需要基于某些条件主动触发信号处理逻辑时。以下是raise
函数的基本用法和说明:
#include <signal.h>
int raise(int sig);
-
sig
:要发送给进程自身的信号编号。这应该是系统支持的信号之一,比如SIGINT
(通常由Ctrl+C生成)、SIGUSR1
或SIGUSR2
(用户自定义信号)等。需要注意的是,某些信号如SIGKILL
和SIGSTOP
不能被raise
调用发送,因为它们不能被捕获或忽略。 -
成功:函数返回0。
-
失败:返回-1,并且会设置
errno
来表明错误原因,例如,如果尝试发送一个无效的信号,或者进程没有权限发送该信号给自己。
下面是一个简单的示例,展示如何使用raise
函数发送一个SIGINT
信号给当前进程,模拟按下Ctrl+C的效果,进而触发预设的信号处理函数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum)
{
printf("catch a sign %d\n", signum);
// 进程可以根据需要在这里执行清理工作或其它操作
}
int main()
{
// 设置信号处理器
signal(SIGINT, signal_handler);
printf("process begin, is coming to send a sign to themselves...\n");
sleep(2); // 等待两秒,让信息显示出来
// 发送SIGINT信号给自身
if (raise(SIGINT) != 0)
{
perror("raise fail");
return 1;
}
printf("continue ...\n"); // 这行可能不会执行,取决于信号处理器的行为
return 0;
}
在这个例子中,程序首先注册了一个信号处理器signal_handler
来处理SIGINT
信号。然后,它通过调用raise(SIGINT)
主动向自己发送了这个信号,从而触发了预先设定好的信号处理逻辑。请注意,如果信号处理器终止了进程,后续的代码可能不会被执行。
abort
我们也可以用abort来终止进程
在C语言编程中,abort
函数用于异常终止当前进程。这是一个标准库函数,通常用于在程序中遇到不可恢复的错误时强制退出。以下是关于abort
函数的基本用法和特性:
#include <stdlib.h>
void abort(void);
- 异常终止:调用
abort
函数会使当前进程立即无条件终止,不执行任何清理工作(如atexit
注册的函数或对象的析构函数)。进程的终止状态将表明是异常终止。 - 信号发送:
abort
函数内部实际上通过发送SIGABRT
信号到调用进程来实现这一行为。这意味着如果进程之前设置了SIGABRT
的信号处理函数,该函数会被调用。但是,除非信号处理函数调用了exit
、_exit
、_Exit
、longjmp
或siglongjmp
等导致进程直接终止的函数,否则一旦信号处理函数返回,abort
将继续终止进程。 - 缓冲区刷新:在发送
SIGABRT
之前,abort
通常会先刷新所有已打开的输出缓冲区,确保缓冲区中的数据被写入到相应的文件或设备中。 - 返回值:由于
abort
调用后不会正常返回到调用点,因此实际上它没有返回值。不过,从技术上讲,它可以被看作是返回一个非零值给操作系统,表示进程异常结束。
下面是一个简单的示例,展示了如何在遇到某种错误条件时使用abort
终止程序:
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include<cstdio>
using namespace std;
void signal_hander(int signum)
{
printf("Caught SIGINT, %d\n",signum);
}
int main()
{
while(true)
{
cout << "process running ... pid: "<< getpid() << endl;
pid_t id = getpid();
signal(2,signal_hander); //换成了九号信号
kill(id,2);
sleep(1);
abort();
}
}
异常传递信号
SIGFPE
我们之前都知道,再写代码的时候不能除以0,否则会报错:
int main()
{
int a = 10;
int b = a / 0;
printf("%d\n",b);
return 0;
}
其实,这也是一个信号:
我们也可以查man的7号手册,查看signal:
说明 / 0的时候,OS发送了8号信号,我们可以来试验一下:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
void signal_hander(int signum)
{
std::cout << "Catch a sign: " << signum << std::endl;
sleep(1);
}
int main()
{
signal(8,signal_hander);
int a = 10;
int b = a / 0;
printf("%d\n",b);
return 0;
}
这个时候程序会进入死循环,因为我们改变了8号信号的默认行为,改为了打印信息,但是在程序中/ 0的问题并没有得到解决,所以会一直打印。
SIGSEGV
空指针引用也是同样的问题,是11号信号:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
void signal_hander(int signum)
{
std::cout << "Catch a sign: " << signum << std::endl;
sleep(1);
}
int main()
{
signal(11,signal_hander);
int *p = nullptr;
*p = 100;
return 0;
}
alarm传递信号
我们可以设定闹钟,设定秒数,在设定几秒钟之后发送14号信号:
在C语言编程中,尤其是在Unix和类Unix系统如Linux中,alarm
函数用于在指定的秒数后向进程发送SIGALRM
信号。这个函数常用于实现定时操作或超时处理。以下是alarm
函数的基本用法和相关说明:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
seconds
:指定等待的秒数,在这个时间结束后,系统会向调用进程发送SIGALRM
信号。如果设置为0,则会取消任何先前设置的闹钟。 -
定时器设置:调用
alarm
函数后,将在指定的秒数后向调用进程发送SIGALRM
信号。如果进程之前已经设置了闹钟,新的调用会覆盖旧的设置。 -
信号处理:进程需要提前通过
signal
或sigaction
函数注册对SIGALRM
信号的处理函数,否则默认情况下,进程会因未处理此信号而终止。 -
返回值:如果之前已设置闹钟,
alarm
函数会返回剩余的秒数;如果没有设置过闹钟,则返回0。
以下是一个简单的使用alarm
函数的示例,展示了如何设置一个1秒的定时器,并注册一个信号处理函数来响应SIGALRM
信号:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
size_t cnt = 0;
void signal_hander(int signum)
{
std::cout << "Catch a sign: " << signum << std::endl;
exit(0);
}
int main()
{
signal(14,signal_hander);
alarm(1);
while(true)
{
std::cout << "alarm: "<< cnt++ << std::endl;
}
return 0;
}
但是如果换一下位置,cnt的数字会更大:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
size_t cnt = 0;
void signal_hander(int signum)
{
std::cout << "alarm: "<< cnt << std::endl;
std::cout << "Catch a sign: " << signum << std::endl;
exit(0);
}
int main()
{
signal(14,signal_hander);
alarm(1);
while(true)
{
//std::cout << "alarm: "<< cnt++ << std::endl;
cnt++;
//std::cout << cnt << std::endl;
}
return 0;
}
第一种写法里同时要处理输出和计算,第二种方法先计算完再打印。所以cnt的数值要大的多。
如果我们重新设了第二个闹钟,但是第一个闹钟的时间还没到,alarm会返回上一个闹钟的剩余时间。
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
size_t cnt = 0;
void signal_hander(int signum)
{
int n = alarm(0);
std::cout << "left time: "<< n << std::endl;
std::cout << "Catch a sign: " << signum << std::endl;
exit(0);
}
int main()
{
signal(14,signal_hander);
alarm(30);
while(true)
{
std::cout <<"running pid :" << getpid() << std::endl;
sleep(1);
}
return 0;
}