信号在生活中随处可见,比如体育比赛中使用的信号枪、我给你传递一个眼神(你懂的哈哈哈),等等。这些信号都有一些共同点:一是简单;而是不能携带大量信息;三是满足某个特设条件才发送。
信号是信息的载体,是Linux/UNIX 环境下,古老而经典的通信方式, 现在依然是主要的通信手段。Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。
进程A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。信号具有这样的特质,由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性,但是对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,本质上都是由内核负责发送的,由内核去处理,我们名义上说是进程A发送信号给进程B,实质上信号是由内核产生,由内核发送,并由内核处理的。进程收到信号要无条件处理信号,并且可以选择忽略(忽略也是对信号的一种处理)、捕捉、处理信号默认的动作等。
Linux内核的进程控制块PCB是一个结构体task_struct,除了包含进程id、状态、工作目录、用户id、组id、文件描述符表、还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
未决信号集就是没有被处理的信号,未决信号集实际上是一个32位数,每一位代表一个信号,当信号产生的时候,就把对应的位反转为1,如果该信号未被处理就反转回0,处理了就保持为1。
而阻塞信号集会影响到未决信号集,比如说我在阻塞信号集中将2号信号为置为1,也就是将2号信号屏蔽,那么未决信号集中2号信号对应的位就会变为1(未决状态),一直阻塞在这种状态。
可以使用 kill –l 命令查看当前系统有哪些可用信号
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关,这些信号名字类似。
每个信号必须要有的四个要素:
可通过 man 7 signal 查看帮助文档获取标准信号信息表
在标准信号中,有一些信号是有三个 "Value",第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。一个 '-' 表示在对应架构上尚未定义该信号。我们主要关注中间的那个值。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或 Mac OS 中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
注意从 man 7 signal 帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. 这里特别强调了SIGKILL 和SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达)。
当程序出现硬件异常会产生信号:
kill命令和kill函数都可以产生信号来杀死进程。kill命令产生信号:kill -SIGKILL pid;kill函数:给指定进程发送指定信号(不一定杀死)。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
#include <signal.h>
int raise(int sig);
/************************************************************
>File Name : test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月23日 星期一 14时20分42秒
************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char* argv[])
{
printf("pid: %d\n", getpid());
sleep(1);
raise(SIGKILL); /* 相当于
kill(getpid(), SIGKILL); */
return 0;
}
#include <stdlib.h>
void abort(void);
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数功能
设置定时器(闹钟),定时给调用进程(也就是自己)发送SIGALRM,来约定进程几秒钟后结束。在指定seconds后,内核会给当前进程发送14)SIGALRM信号,进程收到该信号,默认动作终止。 每个进程都有且只有唯一一个定时器。定时与进程状态无关(自然定时法),就绪、运行、挂起(阻塞、暂停)、终止、僵尸等等无论进程处于何种状态,alarm都会计时。
alarm() arranges for a SIGALRM signal to be delivered to the calling process in seconds seconds.
Signal | Value | Action | Comment |
---|---|---|---|
SIGALRM | 14 | Term | Timer signal from alarm(2) |
Term Default action is to terminate the process. /*终止进程*/
函数参数
函数返回值
alarm() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm. 返回上次定时器剩余的秒数。我们实现约定好多少秒时候发送一个信号,alarm()函数返回距离发送信号还剩余的秒数,如果没有剩余时间或没有约定发送信号返回0。可以这么理解,如果是第一次开启定时器,返回0;如果上一次设定了alarm(5),两秒之后又设置了alarm(3),那么这个alarm()返回上一次定时器剩余的时间,也就是5-2=3秒。
用法示例:
/************************************************************
>File Name : test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月23日 星期一 14时20分42秒
************************************************************/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int ret = alarm(3);
printf("first alarm(3) return: %d\n", ret);
sleep(2);
ret = alarm(5);
printf("second alarm(5) return: %d\n", ret);
while(1)
{
printf("pid: %d\n", getpid());
sleep(1);
}
return 0;
}
编译运行得到结果
示例2:time命令计时与IO优化
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int count = 0;
alarm(1);
while(1)
{
printf("%d\n", count++);
}
return 0;
}
编译运行,使用time命令可以查看程序执行的时间(实际执行时间 = 系统时间 + 用户时间 + 等待时间),time ./a.out
在上面的时间中:
real:总共的时间(自然时间);
user:用户使用时间;
sys:系统时间;
可以看到最大计数到了7572,并且user几乎没有分配到时间,这是因为IO操作(打印屏幕)造成的,我们可以重定向一下输出
可以看到user的时间增加了,并且最大计数达到了306087。实际上程序运行的瓶颈大部分在于IO,优化程序,首先优化IO。
#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
示例1:使用setitimer实现alarm函数定时功能
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, char* argv[])
{
struct itimerval temp = {{0, 0}, {3, 0}};
setitimer(ITIMER_REAL, &temp, NULL); /*ITIMER_REAL,3秒后发送SIGALRM信号*/
while(1)
{
printf("pid: %d\n", getpid());
sleep(1);
}
return 0;
}
编译运行,3秒后闹钟
示例2:周期性定时器
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
/*信号处理回调函数*/
void m_catch(int sig)
{
/*捕捉到信号执行此函数,不杀死进程*/
printf("catch signal: %d\n", sig);
}
int main(int argc, char* argv[])
{
signal(SIGALRM, m_catch/*函数指针做函数参数*/);
/*signal信号捕捉函数,当产生SIGALRM信号的时候,去执行m_catch函数*/
struct itimerval temp = {{2, 0}, {4, 0}}; /*第一次等待4秒,以后每隔2秒发送一个信号*/
setitimer(ITIMER_REAL, &temp, NULL);
while(1)
{
printf("pid: %d\n", getpid());
sleep(1);
}
return 0;
}
编译执行,可以看到第一次隔了4秒捕获到信号,后面周期性的每隔2秒捕获一次信号,不会杀死进程,可以通过ctrl+c杀掉进程。
示例3:setitimer实现alarm函数
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
unsigned int malarm(unsigned int seconds)
{
struct itimerval temp = {{0, 0}, {0, 0}};
struct itimerval ret;
temp.it_value.tv_sec = seconds;
setitimer(ITIMER_REAL, &temp, &ret);
printf("tv_sec: %ld, tv_mirsec: %ld\n", ret.it_value.tv_sec, ret.it_value.tv_usec);
return ret.it_value.tv_sec;
}
int main(int argc, char* argv[])
{
int ret = malarm(5);
printf("malarm() return: %d\n", ret);
sleep(2);
ret = malarm(6);
printf("malarm() return: %d\n", ret);
while(1)
{
printf("pid: %d\n", getpid());
return 0;
}
return 0;
}
编译运行,时间可能会不太准确,这是正常的