- 信号是事件发生时对进程的通知机制,又称为软中断。当程序正常运行时,如果收到一个信号,那么程序的运行流程会被打断,等到执行完处理流程后再继续执行
- 一个进程收到的信号来源有两种,一种是内核发送的,另一种是其他进程或者自己给自己发送的。
- 内核发送信号给进程由以下几种情况产生
- cpu执行了错误的程序指令,如除0或者内存越界等,这时候cpu会收到一个硬件中断然后通知内核,进而内核发送信号给进程
- 用户从进程的控制终端键入了能够产生信号的字符
- 发生了软件事件,如套接字有数据可读或者定时器到期等
- signal调用可以为调用进程注册信号处理函数,这是sigaction的简易版本。signum是要注册的信号类型,handler是处理函数指针,处理函数是无返回值并且只有一个int参数,且signal函数会返回原来的处理函数指针。其原型如下
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 内核在调用信号处理函数时,会将对应的信号编号传递给信号处理函数。当为不同信号注册了一个相同处理函数时,处理函数需要区分到底是哪个信号被触发,这时候可以通过这个参数区分别
- 一个进程可以通过kill调用来向其他进程发送信号,函数原型为
int kill(pid_t pid,int sig)
。如果pid是正数,那么发送信号给进程号为pid的进程;如果pid等于0,将信号发送给与当前进程同组的其他所有进程包括自身;如果pid小于-1,将信号发送给进程组ID等于pid的绝对值的所有进程;如果pid等于-1,将信号发送给除了init进程和调用进程自身的所有其他进程。进程能够成功发送信号还需要一定的权限,如果没有权限发送,则kill调用失败,返回-1并设置errno为EPERM;如果pid是负值,那么只要向其中一个进程发送成功,kill调用就成功其权限规则如下
- 特权级进程可以发送给任何一个进程
- 如果非特权进程的实际用户ID或者有效用户ID和目标进程的实际用户ID或者保存的设置用户ID匹配,那么可以发送
- SIGCONT信号无视发送权限,所有进程都可以发送
- 可以用kill调用检查进程是否存在,只要将sig参数设置为0(表示空信号),然后指定pid为要检查的进程的pid,如果返回-1并且errno为ESRCH,则表示没有此进程
- 对于单线程进程来说,内核会为该进程维护一个信号屏蔽字(列表),里面存放着要屏蔽的信号;对于多线程进程,后面再说。而对于这个信号屏蔽字的相关操作都是凭借sigset_t这个信号集来操控的。首先要调用sigemptyset初始化函数将一个信号集初始化为空(不要尝试自己去用memset等方式初始化,可能会出错),相对的调用sigfillset则是把信号集全部初始化为1,然后调用sigaddset或者sigdelset函数将感兴趣的信号加入信号集或从信号集中删除;另外,sigismember调用可以查看指定信号是否存在于该信号集中
- 如果将被阻塞的信号传递给该进程(对于单线程进程来说),那么该信号的传递会被延后(直到从进程信号掩码中移除该阻塞信号,信号掩码实际上是线程属性,在多线程进程中,每个线程都可以使用pthread_sigmask函数独立检查和修改其信号掩码)
- 对信号掩码的修改有如下几种方式
- 当调用信号处理函数时,会将引发该调用的信号自动添加到信号掩码中(除非调用sigaction时指定SA_NODEFER标志)
- 使用sigaction函数建立信号处理程序时,可以额外添加一组信号掩码,这会使得在调用该处理函数时自动的阻塞这些添加的额外信号
- 调用sigprocmask函数主动的添加或者移除信号掩码
- sigprocmask函数用来直接改变信号掩码,how参数指定了需要怎么样使用这个信号集,有三种选择。一是SIG_BLOCK,将指定的set信号集与添加入当前信号掩码中;二是SIG_UNBLOCK,将set指定的信号从信号掩码中移除;最后是SIG_SETMASK是将set直接赋给信号掩码。set是需要操作的信号集,若oldset参数不为空,则其指向原先阻塞的信号集。函数原型为
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset)
- 当向一个进程发送了一个被阻塞的信号,该信号会被放入该进程的等待信号集中,只有在解除了阻塞后,该信号才会被发送给进程;而使用sigpending函数可以获得当前进程中处于等待状态的所有信号,原型为
int sigpending(sigset_t* set)
。其中,set参数指向等待信号集
- 如果一个信号在被阻塞期间,被发送给一个进程多次;那么等到解除阻塞后,也只会发送该信号给进程一次
- 除了使用signal函数设置某一个信号的处理函数以外,还可以使用sigaction函数;sigaction函数更为复杂,但也提供了更多的控制。sig是要设置的信号,act是一个结构体指针,指向一个信号处理的结构,oact保存原来的设置。关于sigaction结构体,sa_handler指向处理函数,sa_sigaction一般设置为NULL,sa_mask表示额外的信号掩码,sa_flags表示一些特定的标志,sa_restorer指针一般也设置为NULL。对于这个函数的标志属性,后面再讲。函数原型以及结构体如下
#include <signal.h>
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- 调用pause函数可以暂停进程的执行,直到一个信号被发送给该进程为止,原型为
int pause(void)
- 对于一个函数来说,如果能被多个执行线程同时安全的调用,那么就称它为可重入的函数;一般,使用全局变量或者静态变量的函数都不是可重入的,比如说加密函数crypt以及标准库函数malloc等。
- 信号处理函数是异步执行的,所以也是一条单独的执行线程(虽然和主线程不是并发执行)。异步信号安全的函数是指当从处理函数中调用的时候,可以保证其实现是安全的。所以对于编写信号处理函数有以下规则:确保信号处理函数代码本身可重入并且只调用异步信号安全的函数
- 经常会声明一个在信号处理函数和主函数中共享的全局变量,用于指示某些事件的发生;所以为满足代码安全的要求,需要这个全局变量的读写是原子操作并且要求直接从内存中读取而不是寄存器,所以通常这样声明
volatile sig_atmoic_t flag;
但是对该类型变量的++或者--操作不保证原子性
- SIGKILL和SIGSTOP,它们的默认行为不能被改变;SIGKILL信号的默认行为是终止一个进程,SIGSTOP信号的默认行为是暂停一个进程
- SIGCONT信号可以使某些处于停止状态的进程继续运行
- sigsuspend调用会将mask所指向的信号集替换当前的信号掩码,然后挂起进程直到捕获一个信号,并从信号处理函数返回;返回以后,会将信号掩码恢复到原来的值。函数原型为
int sigsuspend(const sigset_t* mask)
- 可以使用sigwaitinfo函数来同步等待信号的到达,该函数挂起进程直到set信号集中的某一信号到达,info参数指向了与该信号相关的详细信息。在调用该函数之前,应该先将所有信号阻塞,否则会发生未定义的行为。该函数原型为
int sigwaitinfo(const sigset_t* set,siginfo_t* info)
,调用成功返回信号数值,否则返回-1
- sigaction的sa_flags标志位中有一个SA_RESTART,这是用来为一些被信号中断的系统调用恢复执行的。如果为某个信号设置了自动重启的标志位且被中断的系统调用能够重启的话,在执行完信号处理程序后就会继续执行系统调用。但是并非所有的系统调用都能支持重启操作,只有在操作慢速设备,像终端,管道,FIFO以及套接字时才能够利用该标志位;除了这个标志位以外,还有一个SA_SIGINFO标志位。如果设置了该标志位,那么在收到信号时可以获得一些额外的附加信息,为了获取该信息,在调用sigaction函数时还需要使用第二个函数指针参数即
void (*sa_sigaction)(int,siginfo_t*,void*)
来替代使用sa_handler;该函数的第二个参数是一个指向附加信息结构体的指针,具体的每个成员的含义可查看手册,该结构体如下
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
- 通常在调用一个阻塞式的系统调用之前都会设置阻塞时间的上限值,不会让其永久的阻塞下去;对于部分函数,本身就有设置阻塞时间的接口。但是还是有以一些调用需要我们手动设置。一般的处理过程如下
- 调用sigaction为SIGALRM信号设置处理函数,并且确保清除SA_RESTART标志,避免系统调用的重新启动
- 调用alarm定时器函数设置一个阻塞时间上限
- 执行阻塞系统调用
- 待系统调用返回后,屏蔽定时器(防止在定时器到期前系统调用就完成的情况)
- 检查系统调用失败时,是否将errno设置为EINTR
- 上面用到的alarm是一个简易定时器函数,其原型为
unsigned int alarm(unsigned int seconds)
,到期时,会向调用进程发送一个SIGALRM信号。需要注意的是调用alarm会覆盖前一个对定时器的设置(如果调用其他的定时器函数也是一样的,比如setitimer),alarm(0)会屏蔽当前的定时器,且alarm的返回值是前一个定时器所剩余的秒数
网友评论