Linux C 信号
信号
总结自unix手册第20 21 22章
信号产生的过程:信号因某事件而产生,稍后(信号的产生和传递之间存在时间间隔,这个时间间隔可能是因为进程正在执行某个系统调用,因此在这个系统调用返回前,信号不会被传递,此时信号处于等待(pending
状态)被传递至指定进程,进程接收信号后作出响应。
基础和概念
信号处置
信号处理器
信号处理器:信号被捕获时调用的函数,该函数由内核代表进程进行调用,保证可以随时打断接收信号的进程。信号处理器的设计应该力求简单。信号处理器形如
void handler(int sig) { }
传入信号的编号,处理器可以根据信号种类的不同选择性的执行一些代码,也就是说,一个信号处理器可以用来处理多种不同的信号。
改变信号处置:signal()
signal
函数不如sigaction
函数优秀,前者在不同unix实现中存在差异,但后者使用更加复杂(功能也更强大)
#include <signal.h> sig_t signal(int sig, sig_t handler)
- sig希望改变处理行为的信号编号
- handler指明改变后信号处理函数,一般这个函数具有这样的形式
改变信号处置:sigaction()
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
-
sig
信号 -
const struct sigaction *act
:是一个指针,指向描述信号新处置方式的数据结构,具体使用见下 -
struct sigaction *oldact
:返回之前信号处置的信息
struct sigaction
struct sigaction { union { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t * , void * ); } sigaction_handler; // 信号的集合,用于记录被阻塞的信号 需要用特定函数处理 sigset_t sa_mask; // 指明信号处理的行为 int sa_flags; void (*sa_restorer) (void); };
-
sigaction_handler
:(匿名联合体,存在于结构体或联合体中,使用时不需要通过联合体名,可以直接使用)-
sa_handler
为函数指针,对应signal()
中的handler
函数- 可以指向
sig_del
,sig_ign
常量之一 - 可以自定信号处理器
- 只有使用自定义函数,
sa_mask
和sa_flags
的设置才有意义
- 只有使用自定义函数,
- 可以指向
-
sa_sigaction
函数指针,可以完成复杂的信号工作
-
-
sa_mask
用于设定信号处理器执行时阻塞的信号。- 使用细节是:内核调用信号处理器之前,将
sa_mask
中设定掩码的信号添加到进程的信号掩码中,直至信号处理器函数返回,再从进程的信号掩码中移除之前添加的信号。
- 使用细节是:内核调用信号处理器之前,将
-
sa_flags
是位掩码,设定需要使用|
-
sa_siginfo
- 发送信号时发送附加信息,信号处理器要声明成
void handler(int sig, siginfo_t *info, void *context);
- 发送信号时发送附加信息,信号处理器要声明成
-
-
sa_restorer
没看到如何用,空下
信号信息的携带:siginfo_t
一个非常复杂的结构体,未了解。
父子信号处理
父进程创建子进程,子进程继承父进程信号处理方式,直到子进程调用exec
函数。exec
函数将调用者的信号处理方式还原成默认。
信号发送
发送信号:kill()
不是去扼杀进程,而是只发送信号,只是早期unix实现中大多数信号的功能是终止进程。
#include <signal.h> int kill(pid_t pid, int sig);
-
pid
标识一个或多个目标进程-
pid > 0
:pid为指定进程 -
pid == 0
:pid发送信号给发送信号所在的进程同组的每个进程- 也就是所有子进程
-
pid < -1
:向组id为-pid的进程组内所有下属进程 -
pid == -1
:调用进程有权发送的所有进程(如果使用ssh连接服务器实验,执行后发现ssh断了)
-
信号发送的权限
发送信号必须满足发送信号的进程和接收信号的进程的用户id相同,或者是发送信号的进程的用户是root。
举例
// 这个是发送信号的代码 #include <sys/types.h> #include <signal.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { int result, i; if(argc > 1) { result = kill(atoi(argv[1]), sigint); printf("result = %d\n", result); } return 0; }
// 这个是接收信号的代码 // 只是一个耗时间的计算,注意不能用sleep代替,因为sleep会使进程挂起 #include <stdio.h> #include <unistd.h> int main() { printf("%d\n", getpid());// 输出此进程的pid int i = 0; double x = 5, y = 0.9548, e = 2.7; for(i = 0; i < 500000000; i++) { x = x * e + (x - i) * e - 3.65 * (y - e); y = y * (x - e); x -= y; } printf("%f\n", x); return 0; }
// 测试kill(0, sigint); #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void handler(int sig); int main(int argc, char **argv) { signal(sigint, handler); pid_t father = getpid(), son[10]; for (size_t i = 0; i < 3; i++) { if (getpid() == father) { son[i] = fork(); if (son[i] != 0) { printf("when %ld : %d -> %d\n", i, father, son[i]); } else { sleep(2);// 子进程休眠,等待父进程发送信号 printf("%d is safe\n", getpid()); } } } if (getpid() == father) { sleep(1); // 如果不加这句话,可能子进程被创建但没来得及执行就被kill了信息 kill(0, sigint); printf("%d is safe\n", getpid()); sleep(2); } return 0; } void handler(int sig) { printf("%d use handler sigint\n", getpid()); }
向自己发送信号:raise()
int raise(int sig);
- 对于单线程:相当于
kill(getpid(), sig);
- 对于非单线程:相当于
pthread_kill(pthread_self(), sig);
由于信号的处理有内核调用信号处理器完成,所以进程使用raise
时,信号立即传递并被处理,甚至在raise
调用返回前。raise
函数调用成功返回0,失败(唯一的失败是einval
,sig
无效)返回非0
值
/** * * 所以这个函数一定是先输出handle done * 再输出result * */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> void handler(int sig) { printf("handle done\n"); } int main(int argc, char **argv) { signal(sigint, handler); int result = raise(sigint); printf("%d\n", result); return 0; }
sigqueue()
向进程发送信号,没用过也没实验过
int sigqueue(pid_t pid, int sig, const union sigval value);
进程组通知:killpg()
#include <signal.h> int killpg(pid_t pgrp, int sig)
向某一进程组的所有成员发送信号,相当于kill(-pgrp, sig);
等待信号:pause()
暂停进程执行,知道信号处理器被调用,中断pause。被中断程序返回-1,并设置errno。
#include <unistd.h> int pause();
显示信号信息:strsignal()
有三种显示的方式
-
sys_siglist
数组,使用sys_siglist[sigxxx]
获得信号的描述 -
strsignal()
函数,返回信号描述的字符串,推荐使用strsignal()
函数,因为会有安全的边界检查,而且该函数设置了地区敏感,可以显示本地语言(没感觉出来) -
psignal()
函数,在标准错误设备上输出msg
信息和sig
的描述
#define _bsd_source // 5.4.0-70-generic下使用这个红出现了警告 // # warning "_bsd_source and _svid_source are deprecated, use _default_source" #include <signal.h> extern const char *const sys_siglist[]; #define _gnu_source #include <string.h> char *strsignal(int sig) #include <signal.h> void psignal(int sig, const char *msg);
信号集
用于表述多个信号的数据结构,sigset_t
,这在linux上以为掩码形式存在。
初始化信号集
sigemptyset()
以空的形式初始化信号集,sigfillset()
以填充所有信号的形式初始化。susv3只要求对sigset_t
赋值即可,sigset_t
其实可以用手动赋值,但这样有损于可移植性,而linux使用函数实现,增强了可移植性。
#include <signal.h> int sigemptyset(sigset_t *__set) int sigfillset(sigset_t *__set)
操作信号集
分别有从信号集中添加和删去某个信号,或是判断这个信号是否在信号集中
#include <signal.h> int sigaddset(sigset_t *set, int sig) int sigdelset(sigset_t *set, int sig) int sigismember(sigset_t *set, int sig); int sigpending(sigset_t *set);
-
sigismember
:sig
存在于set
中返回1
,否则返回0
-
sigpending
:返回调用进程处于等待的信号
gnu c的拓展
需要在宏中添加
#define _gnu_source
具体的三个函数这里没有写
信号掩码
信号传递的阻塞:
每个进程拥有一个信号掩码,由内核维护,记录着需要阻塞的信号。如果内核发送该进程的信号掩码中记录的信号给该进程,那么这个信号会被阻塞,除非从进程掩码中移除。更进一步,信号掩码可以细致到线程级别
sigprocmask函数
- 修改该进程的信号掩码
- 获得该进程的信号掩码
- 设置
set
为null
且how
为sig_block
,可以用oldset
中获得信号掩码
- 设置
#include <signal.h> int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset)
-
how
指定了函数的具体行为-
sig_block
,将set
信号集中的信号添加到信号掩码中 -
sig_unblock
,将set
中的信号从掩码中移除 -
sig_setmask
,将set
信号集赋值给掩码
-
-
set
指定的程序需要处理的信号的信号集 -
oldset
得到修改之前的信号掩码
信号处理器
概念
信号处理器和主程序是两条独立的线程,同属于同一进程。
可重入函数
同一进程多个线程看同时安全(产生预期的结果)调用的函数
要求
- 只是用本地变量
不可重入函数的特点
- 使用全局变量和静态数据结构可能是不可重入
- 使用静态分配的内存,这次调用会覆盖上次调用的信息
常见不可重入函数举例
- 不可重入
- malloc函数族
异步信号安全函数
可重入或信号处理器函数无法中断的函数
计时器与休眠
定时器精度问题:没有写,见unix编程手册23.2章和10.6章
计时器
linux对每个进程设置3个计时器计时器的种类有:
真实计时器 | 虚拟计时器 | 实用计时器 | |
---|---|---|---|
c语言中的值 | itimer_real |
itimer_virtual |
itimer_prof |
记录时间 | 程序运行的总时间 | 在用户态的时间之和 | 在用户态和内核态的时间之和 |
到期发送信号 | sigalrm |
sigvtalrm |
sigprof |
对这些信号的默认处理是终止进程,除非自定义信号处理函数。
间隔计时器
计时器数据结构:
struct itimerval { struct timeval it_interval; struct timeval it_value; }; struct timeval // 时间的数据结构 { long tv_sec; // seconds long tv_usec; // microseconds };
系统使用settimer
创建定时器
#include <sys/times.h> int setitimer(int which, const struct itimerval *new, struct itimerval *old)
-
which
指明需要创建哪种计时器- 使用c语言预定义值指明
-
new
-
it_value
指明定时器到期的计时时间- 两个值都为
0
表示屏蔽计时器 - 值表示初始的间隔时间,即第一次发送信号的时间间隔
- 两个值都为
-
it_interval
指明定时器是否是周期性定时器- 为
0
时不表示间隔时间是0
,而是表示计时器不是周期性的,是一次性的 - 不为
0
时表示计时value后每次间隔interval再发送信号
- 为
-
-
old
-
old
不为null
时,则该值指向函数设定计时器时的前一个设置,用于计时器设定的还原
-
函数行为:计时器会从new.it_value
开始倒计时直到0
为止,递减至0
时发送信号,若new.it_interval != 0
,重置并开始计时
一个进程只能拥有三种计时器的一种,所以之后再次调用setitimer
时会修改上一次的设定值
#include <sys/times.h> int getitimer(int which, struct itimerval *value)
获得当前计时器的状态,类似于setitimer
中的old
#include <unistd.h> unsigned int alarm(unsigned int seconds)
设定一个sedonds
秒后到期的计时器,到期时发送sigalrm
信号,(这也会覆盖之前的设定,alarm(0)
表示屏蔽所有计时器)
休眠
#include <unistd.h> unsigned int sleep(unsigned int seconds); // 休眠seconds秒 void usleep(unsigned long usec); // 休眠usec * 10 ^ -6秒 // 这两个函数已经进行一次抽象了 // 等价于调用 unsigned int alarm(unsigned int seconds); int pause(void);
sleep
函数正常休眠,返回0
,如果因为信号中断休眠,返回剩余休眠的时间。
高精度
#include <time.h> int nanosleep(const struct timespec *requested_time, struct timespec *remaining) struct timespec { long tv_sec; /* seconds. */ long tv_nsec; /* nanoseconds. */ };
该函数的实现不依赖与信号,so???
- requested_time
- 指明休眠时间,支持纳秒级别
下一篇: Python web开发(一):认识网页