此为牛客Linux C++和黑马Linux系统编程课程笔记。
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
#include
#include
int kill(pid_t pid, int sig);
功能:给任何的进程或者进程组pid, 发送任何的信号 sig
参数:
< 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
如kill(getppid(), 9);
能够杀死父进程;kill(getpid(), 9);
能够杀死当前进程。
#include
#include
int raise(int sig);
功能:给当前进程发送信号;
参数:sig : 要发送的信号;
返回值:成功 0, 失败 非0。
相当于kill(getpid(), sig);
#include
#include
void abort(void);
功能: 发送SIGABRT(编号为6)信号给当前的进程,杀死当前进程;
相当于kill(getpid(), SIGABRT);
或raise(SIGBRT);
。
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
#include
unsigned int alarm(unsigned int seconds);
功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM。
参数:seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
返回值:
常用:使用alarm(0)取消定时器,返回旧闹钟余下秒数。
每个进程都有且只有唯一个定时器。 比如:进程先执行了alarm(10),2秒后又执行了一个alarm(5),alarm(5)的返回值是8,因为之前有定时器,返回的是之前定时器的剩余时间。然后从现在起该进程还是只有一个定时器,定时5秒,因为后来的定时器会刷新之前的定时器。
注意,alarm定时是与与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
看以下示例程序:
#include
#include
int main()
{
int i;
alarm(1);
for(i = 0; ; i++) {
printf("%d\n", i);
}
return 0;
}
用定时器让程序执行1s后停止。
我们用time ./alarm
来查看该程序的运行时间:
可以看到实际运行时间几乎是1秒,但是发现用户时间和系统时间加起来与总的运行时间不同,这是为什么呢。
实际执行时间 = 系统时间 + 用户时间 + 等待时间。程序的很多时间浪费在printf上了。
#include
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时。
参数:
struct itimerval {
// 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval {
// 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
如以下示例程序能够实现延迟3秒,每2秒发送一次信号。
#include
#include
#include
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
由于还没有介绍signal信号捕捉函数,setitimer发出的信号让程序终止,所以无法演示其周期性发送信号的功能,接下来介绍signal函数。
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:设置某个信号的捕捉行为
注意:并不是该函数来捕捉信号,该函数只是向内核注册对某个信号的捕捉行为。
参数:
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
返回值:
注意:SIGKILL 和 SIGSTOP不能被捕捉,不能被忽略。
在setitimer的示例代码中加入signal后,示例代码如下:
void myfunc(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
signal(SIGALRM, myfunc);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
signal的第二个参数传入函数地址,当当前进程捕捉到SIGALRM信号时,将执行程序员自定义的myfunc函数,myfun函数的int类型参数是捕捉到的信号的值(编号)。
程序运行结果如下:
程序运行3秒后第一次发出信号,程序输出一次,然后每隔2秒发出一次信号。
一个进程的PCB中除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集:
以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
一个用到以上函数的示例程序如下:
#include
#include
int main() {
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set 里
int ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断SIGINT是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
return 0;
}
之前的信号集函数均是对自定义的信号集进行操作,那如何修改内核中的阻塞信号集呢?可以使用sigprocmask函数,用自定义的信号集设置内核阻塞信号集。
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)。
参数:
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(假设内核中默认的阻塞信号集是mask, mask | set)。
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞(相当于 mask = mask & ~set)。
SIG_SETMASK: 用set覆盖内核中原来的值。
返回值: 成功:0 ;失败:-1,并设置错误号。
#include
int sigpending(sigset_t *set);
功能:获取内核中的未决信号集。
参数:set,传出参数,保存的是内核中的未决信号集中的信息。
sigaction函数通常用于替代signal函数,用来捕捉信号,同时自定义信号的处理动作。
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或者改变信号的处理。信号捕捉。
参数:
返回值: 成功 0 失败 -1
其中参数act的类型sigaction结构体定义如下:
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
// 不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
// 被废弃掉了
void (*sa_restorer)(void);
};
其中sa_sigaction和sa_restorer我们基本用不到,所以掌握以下三个即可:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作。
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。
该函数与signal函数最大的区别就在于sa_mask上,sa_mask是程序员自定义的一个信号集,该信号集充当调用信号处理函数时的一个临时的阻塞信号集,也就是说:
进程正常运行时,默认PCB中有一个信号屏蔽字(阻塞信号集),假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
示例程序如下:
#include
#include
#include
#include
void catchFunc(int signo) {
printf("捕捉到了信号:%d \n", signo);
}
int main() {
struct sigaction act;
act.sa_handler = catchFunc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号
int res = sigaction(SIGINT, &act, NULL);
if(res == -1) {
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}
执行结果如下:
每次在终端输入ctrl+c(产生SIGINT信号)时,输出:捕捉到了信号2。
当在键盘中输入ctrl+\(产生SIGQUIT)时, 程序退出。那么有个问题,程序中不是已经设置了sigaddset(&act.sa_mask, SIGQUIT);
来屏蔽信号了吗?为什么输入ctrl+\时, 程序依然会退出?是因为sigaction函数设置的sa_mask只在信号处理函数执行中生效,输出语句后信号处理函数以及执行完毕。
再看下面示例程序:该程序让信号处理函数睡眠10秒。
#include
#include
#include
#include
#include
void catchFunc(int signo) {
printf("捕捉到了信号:%d \n", signo);
sleep(10);
printf("-----finish-----");
}
int main() {
struct sigaction act;
act.sa_handler = catchFunc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号
int res = sigaction(SIGINT, &act, NULL);
if(res == -1) {
perror("sigaction error");
exit(1);
}
return 0;
}
运行程序后,输入ctrl+c,终端输出如下:
此时10秒以内,依然在执行信号捕捉函数catchFunc,也就是说当前sa_mask是生效的。此时我们输入crtl+\:
程序并没有退出,因为此时sa_mask中屏蔽了SIGQUIT信号。等待10秒过后:
发现程序自动退出,这是因为10秒过后信号捕捉函数catchFunc执行完毕,临时的阻塞信号集(sa_mask)失效,此时生效的是原PCB中的阻塞信号集,未决信号集(SIGQUIT处的值为1)查询到后阻塞信号集中SIGQUIT处的值是0后,SIGQUIT信号递达,程序退出。
这里还有一个值得注意的细节:
当信号捕捉函数catchFunc执行时,我输入了多个ctrl+c后,信号捕捉函数执行完毕后,只输出了一个“捕捉到了信号:2”,这是因为我们无论向当前进程发出多少个相同信号,未决信号集的对应位都是1,无法记录相同信号的数量,所以当临时阻塞信号集被取消后,只输出了一个“捕捉到了信号:2”。有以下结论:
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)。