主题: Linux系统学习笔记
« Linux系统学习笔记:进程
» Linux系统学习笔记:线程
本篇总结信号。信号是软件中断,它提供了一种处理异步事件的方法。
Contents
- 信号
- 信号名字和映射
- 中断的系统调用
- 不可重入函数
- 信号集
- 发送信号
- 挂起进程
- 信号处理
- 进程同步
信号
前面已经介绍过信号。信号被定义为正整数。很多条件可以产生信号:
- 用户按某些终端键时引发终端产生信号。
- 硬件异常产生信号。
- 进程调用 kill 函数可将信号发送给另一个进程或进程组。发送信号和接收信号的进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
- 用户可用 kill 命令发送信号给其他进程。
- 检测到某种软件条件已经发生并应通知有关进程时也产生信号。
信号是异步事件的经典实例。有三种处理信号的方式:
- 忽略信号。 SIGKILL 和 SIGSTOP 信号是不能忽略的,它们提供了使进程终止或停止的可靠方法。
- 捕捉信号。这需通知内核在某种信号发生时调用一个用户函数,用户函数中包含希望对事件进行的处理。 SIGKILL 和 SIGSTOP 信号不能被捕捉。
- 执行系统默认动作。大多数信号的系统默认动作是终止进程。
执行程序时, exec 函数会将原先设为要捕捉的信号改为执行默认动作。
使用 fork 创建进程时,子进程继承父进程的信号处理方式。
信号名字和映射
有一些办法可以获得信号的名字。使用数组 sys_siglist 可以获得指向信号字符串名字的指针。 psignal类似于 perror ,将有关信号的说明输出到标准错误输出。 strsignal 则返回说明字符串。
#include <signal.h> /* 输出s: sig info\n到标准错误输出 */ void psignal(int sig, const char *s); #include <string.h> /* 返回指向描述信号的字符串的指针 */ char *strsignal(int sig);
中断的系统调用
系统调用分为低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用。包括:
- 读有些类型文件(管道、终端、网络),且数据不存在。
- 写这些类型文件,不能立即接受数据。
- 打开某些类型文件,在某种条件发生之前(终端等待)。
- pause 和 wait 函数。
- 某些 ioctl 操作。
- 某些进程间通信函数。
磁盘I/O只会暂时阻塞,所以并非低速系统调用。
早期的UNIX系统中,进程在执行低速系统调用而阻塞期间捕捉到一个信号,则系统调用就被中断不再执行,系统调用返回出错, errno 设置为 EINTR 。现在引入了自动重启动功能,Linux系统默认重启动由信号中断的系统调用。
不可重入函数
进程捕捉到信号时,进程在执行的指令序列就被信号处理程序临时中断,执行完信号处理程序中的指令后,若从信号处理程序返回,则继续执行原来在执行的指令序列。但这可能对进程是具有破坏性的,如正在执行 malloc 函数或一些结果存放在静态变量中的函数时被中断,再返回继续执行时就可能得到错误的结果甚至对进程造成破坏。
大多数函数是不可重入的,有三种原因:
- 使用了静态数据结构。
- 调用 malloc 和 free 。
- 标准I/O函数,它们通常使用不可重入的全局数据结构。
信号集
信号集用来告诉内核不产生该信号集中的信号。下面函数用来处理信号集结构 sigset_t 。
#include <signal.h> /* 初始化set为空集 * @return 成功返回0,出错返回-1 */ int sigemptyset(sigset_t *set); /* 初始化set包含所有信号 * @return 成功返回0,出错返回-1 */ int sigfillset(sigset_t *set); /* 将signum添加到set中 * @return 成功返回0,出错返回-1 */ int sigaddset(sigset_t *set, int signum); /* 从set中删除signum * @return 成功返回0,出错返回-1 */ int sigdelset(sigset_t *set, int signum); /* signum在set中返回1,否则返回0,出错返回-1 */ int sigismember(const sigset_t *set, int signum);
前面提到进程的信号屏蔽字规定了对进程阻塞的信号集,用 sigprocmask 函数来检测和更改信号屏蔽字。
#include <signal.h> /* 改变进程的信号屏蔽字 * @return 成功返回0,出错返回-1 */ int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
- how
-
指定如何修改屏蔽字。
- SIG_BLOCK :添加 set 中的信号到屏蔽字中。
- SIG_UNBLOCK :从信号屏蔽字中删除 set 中的信号。
- SIG_SETMASK :将信号屏蔽字设为 set 。
- set
- 待设信号集,若为空则不改变信号屏蔽字。
- oldset
- 若非空,将当前信号屏蔽字保存在 oldset 中。
sigpending 函数返回当前阻塞信号的信号集。
#include <signal.h> /* 获取当前阻塞信号的信号集 * @return 成功返回0,出错返回-1 */ int sigpending(sigset_t *set);
例:
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include "error.h" static void sig_quit(int); int main(void) { sigset_t newmask, oldmask, pendmask; if (signal(SIGQUIT, sig_quit) == SIG_ERR) err_sys("can't catch SIGQUIT"); /* Block SIGQUIT and save current signal mask. */ sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error"); sleep(5); /* SIGQUIT here will remain pending */ if (sigpending(&pendmask) < 0) err_sys("sigpending error"); if (sigismember(&pendmask, SIGQUIT)) printf("\nSIGQUIT pending\n"); /* Reset signal mask which unblocks SIGQUIT. */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); printf("SIGQUIT unblocked\n"); sleep(5); /* SIGQUIT here will terminate */ exit(0); } static void sig_quit(int signo) { printf("caught SIGQUIT\n"); if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) err_sys("can't reset SIGQUIT"); }
发送信号
kill 函数发送信号给进程或进程组, raise 函数发送信号给自己。
#include <sys/types.h> #include <signal.h> /* 发送信号给进程或进程组 * @return 成功返回0,出错返回-1 */ int kill(pid_t pid, int sig); #include <signal.h> /* 发送信号给进程或进程组 * @return 成功返回0,出错返回-1 */ int raise(int sig);
raise(sig) 相当于 kill(getpid(), sig) 。
pid 参数有四种情况:
- > 0 时,将信号发送给进程ID等于 pid 的进程。
- = 0 时,将信号发送给和发送进程在同一进程组的所有进程,且需要具有向这些进程发送信号的权限。
- < 0 时,将信号发送给进程组ID等于 pid 绝对值的进程,且需要具有向这些进程发送信号的权限。
- = -1 时,将信号发送给所有进程,且需要具有向这些进程发送信号的权限。
所有进程中不包括系统进程。
关于权限,超级用户可将信号发送给任一进程,非超级用户发送者的实际或有效用户ID须等于接收者的实际或有效用户ID。一个特例是发送信号 SIGCONT 时进程可将它发送给同一会话的所有进程。
0为空信号,做参数时不发送信号。 kill 向不存在的进程发送空信号时返回-1,设置 errno 为 ESRCH 。
alarm 函数可以设置一个闹钟,当闹钟超时时产生 SIGALRM 信号,默认终止调用 alarm 的进程。
#include <unistd.h> /* 设置一个计时器 * @return 0或上次设置的闹钟时间的剩余秒数 */ unsigned int alarm(unsigned int seconds);
每个进程只能有一个闹钟。如果调用 alarm 时,有一个未超时的闹钟,则该闹钟被代替或取消(本次调用参数为0时),剩余时间作为本次调用的返回值。
abort 函数可以发送 SIGABRT 信号到调用进程,相当于 raise(SIGABRT) ,它使程序终止。
挂起进程
pause 函数使调用进程挂起直到捕捉到一个信号。
#include <unistd.h> /* 挂起进程 * @return -1,并设errno为EINTR */ int pause(void);
执行了信号处理程序并返回时, pause 才返回。
sigsuspend 函数可以将设置信号屏蔽字和挂起进程组成为一个原子操作,这样就可以正确地实现对某个信号解除阻塞,然后挂起等待以前被阻塞的信号的发生。
#include <signal.h> /* 将进程的信号屏蔽字设为mask,挂起进程,捕捉到信号返回时恢复信号屏蔽字为之前的值 * @return -1,并设errno为EINTR */ int sigsuspend(const sigset_t *mask);
sleep 函数也挂起进程,直到经过指定秒数或捕捉到一个信号并从信号处理程序返回。 usleep 提供微秒级的时间粒度。
#include <unistd.h> /* 挂起进程 * @return 0或剩余秒数 */ unsigned int sleep(unsigned int seconds); /* 挂起进程 * @return 成功返回0,出错返回-1 */ int usleep(useconds_t usec);
还有一个 nanosleep 函数,它可以提供纳秒级的时间粒度。
#include <time.h> /* 挂起线程 * @return 成功返回0,中断或出错返回-1 */ int nanosleep(const struct timespec *req, struct timespec *rem);
req 指定休眠时间, rem 不为 NULL 时被设置为剩余时间。结构 timespec 的定义如下:
struct timespec { time_t tv_sec; /* 秒数 */ long tv_nsec; /* 纳秒数 */ }
信号处理
sigaction 函数可以检查或修改指定信号的处理动作,它取代了 signal 函数。
#include <signal.h> /* 检查或修改信号的处理动作 * @return 成功返回0,出错返回-1 */ int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
若 act 非空,则修改信号的动作;若 oldact 非空,则获取信号的上一个动作。
结构 sigaction 的定义如下:
struct sigaction { void (*sa_handler)(int); /* 信号捕捉函数,或SIG_IGN, SIG_DFL */ void (*sa_sigaction)(int, siginfo_t *, void *); /* 替代动作 */ sigset_t sa_mask; /* 额外要阻塞的信号 */ int sa_flags; /* 信号选项 */ void (*sa_restorer)(void); };
在 sa_handler 被调用之前, sa_mask 和 signum 信号被加到信号屏蔽字中,从信号捕捉函数返回时再将信号屏蔽字恢复为原值。
sa_flags 字段的可选项包括:
- SA_INTERRUPT ,该信号中断的系统调用不会自动重启动。
- SA_NOCLDSTOP ,对 SIGCHLD 信号,子进程停止或从停止继续时不产生该信号;子进程终止时仍产生该信号。
- SA_NOCLDWAIT ,对 SIGCHLD 信号,子进程终止时不创建僵死进程。若进程调用 wait ,则进程阻塞,直到所有子进程终止,然后返回-1, errno 设为 ECHILD 。
- SA_NODEFER ,执行信号捕捉函数时,不阻塞 signum ,除非在 sa_mask 中包含 signum 。
- SA_ONSTACK ,若用 sigaltstack 函数声明了一替换栈,则将 signum 传递给替换栈上的进程。
- SA_RESETHAND ,在信号捕捉函数入口处,将 signum 的处理方式复位为 SIG_DFL ,并清除SA_SIGINFO 标志,但不能复位 SIGILL 和 SIGTRAP 的配置。
- SA_RESTART ,该信号中断的系统调用会自动重启动。
- SA_SIGINFO ,对信号处理程序提供附加信息:指向 siginfo 结构的指针和指向进程上下文标识符的指针。
使用了 SA_SIGINFO 选项时,使用替代的 sa_sigaction 信号捕捉函数。 siginfo_t 结构的定义参考手册。
可以用 sigaction 函数实现 signal 函数:
sighandler_t signal(int signum, sighandler_t handler) { struct sigaction action, oldaction; action.sa_handler = handler; sigemptyset(&action.sa_mask); action.sa_flags = SA_RESTART; if (sigaction(signum, &action, &oldaction) < 0) return SIG_ERR; return (oldaction.sa_handler); }
进程同步
可以用信号实现父子进程间的同步。
下面是用信号解决竞争条件的版本。 SIGUSR1 由父进程发送给子进程, SIGUSR2 由子进程发送给父进程。这个版本适合在等待信号时休眠,如果在等待信号时希望调用其他系统函数,一般需要使用多线程。
static volatile sig_atomic_t sigflag; /* set nonzero by sig handler */ static sigset_t newmask, oldmask, zeromask; static void sig_usr(int signo) /* one signal handler for SIGUSR1 and SIGUSR2 */ { sigflag = 1; } void TELL_WAIT(void) { if (signal(SIGUSR1, sig_usr) == SIG_ERR) err_sys("signal(SIGUSR1) error"); if (signal(SIGUSR2, sig_usr) == SIG_ERR) err_sys("signal(SIGUSR2) error"); sigemptyset(&zeromask); sigemptyset(&newmask); sigaddset(&newmask, SIGUSR1); sigaddset(&newmask, SIGUSR2); /* Block SIGUSR1 and SIGUSR2, and save current signal mask. */ if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error"); } void TELL_PARENT(pid_t pid) { kill(pid, SIGUSR2); /* tell parent we're done */ } void WAIT_PARENT(void) { while (sigflag == 0) sigsuspend(&zeromask); /* and wait for parent */ sigflag = 0; /* Reset signal mask to original value. */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); } void TELL_CHILD(pid_t pid) { kill(pid, SIGUSR1); /* tell child we're done */ } void WAIT_CHILD(void) { while (sigflag == 0) sigsuspend(&zeromask); /* and wait for child */ sigflag = 0; /* Reset signal mask to original value. */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); }