Linux - 信号
信号的概念
共性:1. 简单 2. 不能携带大量信息3. 满足某个特设条件才发送。
机制:
- 由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。
- 每个进程收到的所有信号,都是由内核负责发送的,内核处理。
信号的产生与处理方式
- 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
信号的处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
阻塞信号集(信号屏蔽字):
- 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)。
未决信号集:
- 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号四要素:
- 编号 2. 名称 3. 事件 4. 默认处理动作
kill函数/命令产生信号
int kill(pid_t pid, int sig);//给指定进程发送指定信号
int raise(int sig);//给当前进程发送指定信号 raise(signo) == kill(getpid(), signo);
void abort(void); //给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件
1
2
3
2
3
软件条件产生信号
unsigned int alarm(unsigned int seconds);//返回0或剩余的秒数,无失败。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
//设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
1
2
3
2
3
信号处理函数
信号集操作函数
- 内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。
信号集设定
sigset_t set; // typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); 将某个信号集清0 成功:0;失败:-1
int sigfillset(sigset_t *set); 将某个信号集置1 成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); 将某个信号加入信号集 成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); 将某个信号清出信号集 成功:0;失败:-1
int sigismember(const sigset_t *set, int signum);判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错:-1
1
2
3
4
5
6
2
3
4
5
6
处理函数:
sigprocmask函数:用来屏蔽或解除屏蔽信号。其本质,读取或修改进程的信号屏蔽字(PCB中)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失败:-1,
how:SIG_BLOCK;SIG_UNBLOCK;SIG_SETMASK (屏蔽,解除屏蔽,新屏蔽集),oldset为旧的信号屏蔽集。
调用sigprocmask(SIG_SETMASK, &oldmask, NULL);来恢复原先的信号屏蔽集。
sigpending函数:读取当前进程的未决信号集
int sigpending(sigset_t *set); set传出参数。 返回值:成功:0;失败:-1,设置errno
1
2
3
4
5
6
7
2
3
4
5
6
7
信号捕捉函数:
signal函数:注册一个信号捕捉函数。
typedef void (*sighandler_t)(int); //去掉typedef,即定义了一个函数指针,指向的函数接受一个整型参数并返回一个无类型指针
sighandler_t signal(int signum, sighandler_t handler);
sigaction函数:修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
1
2
3
4
5
6
2
3
4
5
6
竞态条件(时序竞态)
pause函数:int pause(void); 返回值:-1 并设置errno为EINTR,仅有出错返回
- 调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
时序问题分析:
- 利用alarm和pause来实现sleep函数功能时,可能alarm函数调用完,就失去CPU。等再次得到CPU时, 直接去执行SIGALRM的信号处理函数,即pause永远等不到被外来信号中断,程序一直死等待。
解决时序问题:
- 可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
int sigsuspend(const sigset_t *mask); 挂起等待信号。
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
1
2
2
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽, 然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态, sigsuspend函数返回后仍然屏蔽该信号。
全局变量异步I/O
- 在父子进程中对全局变量同时进行操作时,可能导致程序运行出错。解决方法:利用锁机制。
可/不可重入函数
- 一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。不可重入函数大多是在函数中使用了全局变量以及静态变量等,导致程序出错。
注意:信号捕捉函数应设计为可重入函数
SIGCHLD信号
SIGCHLD的产生条件:
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
SIGCHLD信号注意问题
- 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。
- 注意注册信号捕捉函数的位置。
- 应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。
信号传参
发送信号传参:
sigqueue函数对应kill函数,但可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);成功:0;失败:-1,设置errno
1
捕捉函数传参:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
当注册信号捕捉函数,希望获取更多信号相关信息,不应使用sa_handler而应该使用sa_sigaction。 但此时的sa_flags必须指定为SA_SIGINFO。siginfo_t是一个成员十分丰富的结构体类型,可以携带各种与信号相关的数据。
中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用。
- 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断, 不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait...
- 其他系统调用:getpid、getppid、fork...