APUE学习笔记(十)信号
10.1 信号概念
mac和linux都支持31种信号。不存在编号为0的信号。
产生信号的方式:
- 当用户按某些终端键时引发终端产生的信号
- 硬件异常产生信号:除数为0、无效的内存引用等
- 用户可用kill命令将信号发送给其他进程
- 进程调用kill函数可将任意信号发送给另一个进程或进程组
- 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号
处理信号的方式:
- 忽略此信号。SIGKILL和SIGSTOP这两种信号不能被忽略,它们向内核和超级用户提供了使进程终止的可靠方法。
- 捕捉信号。不能捕捉SIGKILL和SIGSTOP信号。
- 执行系统默认动作。对大多数信号的系统默认动作是终止该进程。
10.2 signal函数
用来指定信号处理程序,返回值为上一次设置的处理程序指针。signal函数的限制是不改变信号的处理方式就不能确定信号的当前处理方式。
当执行一个程序时,所有信号的状态都是系统默认或忽略。
exec函数将原先设置为要捕捉的信号都更改为默认动作。
shell自动将后台进程对中断和退出信号的处理方式设置为忽略。
当一个进程调用fork时,其子进程继承父进程的信号处理方式。
例子
#include "apue.h"
static void sig_usr(int );
int main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR) //安装信号处理程序
err_sys("cannot catch sigusr1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("cannot catch sigusr2");
for (;;) {
pause(); //使进程在后台运行直到收到一个信号
}
}
static void sig_usr(int signo) {
if (signo == SIGUSR1)
printf("receive SIGUSR1\n");
else if (signo == SIGUSR2)
printf("receive SIGUSR2\n");
else
err_dump("receive signal %d\n", signo);
}
10.3 中断的系统调用
低速系统调用是可能会使进程永远阻塞的一类系统调用,如等待读取某些类型的文件,pause函数,某些ioctl操作等。
早期unix系统,如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。
为了不必处理被中断的系统调用,引入了自动重启动的系统调用,包括ioctl、read、readv、write、writev、wait 和waitpid。
10.4 可重入函数
指在信号处理程序中保证调用安全的函数。进程捕捉到信号时,正在执行的正常指令序列就被临时中断。在信号处理程序中不能判断捕捉到信号时进程执行到何处。如果在信号处理程序中调用不可重入函数,可能造成预期外的结果。
不可重入函数通常:
- 使用静态数据结构
- 调用malloc或free
- 标准I/O函数,使用全局数据结构
例子
#include "apue.h"
#include <pwd.h>
static void my_alarm(int signo) {
struct passwd *ptr;
printf("in signal handler \n");
if ((ptr = getpwnam("root")) == NULL)
err_sys("getpwan error");
alarm(1);
}
int main(void)
{
struct passwd *ptr;
signal(SIGALRM, my_alarm);
alarm(1);
for (;;) {
if ((ptr = getpwnam("lifan")) == NULL) //getpwnam 不可重入
err_sys("error");
if (strcmp(ptr->pw_name, "lifan") != 0)
printf("return value wrong, %s\n", ptr->pw_name);
}
}
10.5 SIGCHILD
SIGCLD是System V的一个信号名,SIGCHLD是BSD的信号名。现在两者是作为同义词存在的。
SIGCHILD是子进程退出时对父进程发送的信号。父进程对SIGCHILD的处理方式:
- 若设置为SIG_IGN,则丢弃所有子进程状态,不会产生僵死进程
- 若设置为捕捉,则在有子进程准备好被等待时,调用信号处理程序
例子
#include "apue.h"
#include <sys/wait.h>
static void sig_cld(int );
int main(void)
{
pid_t pid;
if (signal(SIGCHLD, sig_cld) == SIG_ERR) //安装信号处理程序
err_sys("cannot catch sigusr1");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) {
sleep(2);
_exit(0);
}
pause();
exit(0);
}
static void sig_cld(int signo) {
pid_t pid;
int status;
printf("SIGCLD received\n");
if (signal(SIGCHLD, sig_cld) == NULL)
perror("signal error");
if ((pid = wait(&status)) < 0)
perror("wait error");
printf("pid = %d\n", pid);
}
10.6 信号术语
-
当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。
-
在信号产生和递送之间的时间间隔内,称信号是未决的。
-
进程可以阻塞信号递送 。内核在递送一个原来被阻塞的信号给进程时才决定对它的处理方式。
-
对于发生多次的信号,大多数UNIX并不对信号排队, 而是只递送这种信号一次。
-
同时发生的多个信号,建议先递送与进程当前状态有关的信号,如SIGSEGV。
-
每个进程都有一个信号屏蔽字,规定了当前要阻塞递送到该进程的信号集。
10.7 kill和raise
kill函数将信号发送给进程或进程组,raise函数则允许进程向自身发送信号。
进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际用户ID 或有效用户 ID 必须等于接收者的实际用户 ID或有效用户ID。
pid不同取值的区别
1. pid >0,发送对应的pid进程
2. pid = -1, 发送给进程有权限的所有的进程
3. pid = 0, 发送给同一进程组的进程
4. pid < 0,发送给进程组id等于pid绝对值的
例子
#include "apue.h"
#include <sys/wait.h>
static void sig_cld(int );
int main(void)
{
pid_t pid;
if (signal(SIGUSR1, sig_cld) == SIG_ERR)
err_sys("cannot catch sigusr1");
if (signal(SIGUSR2, sig_cld) == SIG_ERR)
err_sys("cannot catch sigusr1");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) {
pause();
_exit(0);
}
kill(pid, SIGUSR1);
raise(SIGUSR2);
kill(getpid(), SIGUSR2);
exit(0);
}
static void sig_cld(int signo) {
if (signo == SIGUSR1)
printf("received sigusr1 %d, pid %d\n", signo, getpid());
else if (signo == SIGUSR2)
printf("received sigusr2 %d, pid %d\n", signo, getpid());
}
10.8 alarm和pause
alarm函数可以设置一个定时器,当定时器超时时产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。每个进程只能有一个闹钟时间,多次调用则用新的时间代替旧的时间。
pause函数使调用进程挂起直至捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回。
alarm被用来实现sleep函数,或者为可能发生阻塞的调用设置超时时间。
例子1, sleep
#include "apue.h"
#include <sys/wait.h>
typedef void Sigfunc(int);
static void sig_alarm(int);
unsigned int sleep1(unsigned int);
int main(void)
{
printf("begin\n");
sleep1(2);
printf("end\n");
}
static void sig_alarm(int signo) {}
unsigned int sleep1(unsigned int seconds) {
unsigned int old_sec;
Sigfunc *sigPtr;
if ((sigPtr = signal(SIGALRM, sig_alarm)) == SIG_ERR)
return(seconds);
if ((old_sec = alarm(seconds)) > seconds) { //如果以前设置过alarm,则判断一下余下的超时时间
alarm(old_sec);
}
pause();
signal(SIGALRM, sigPtr); //恢复原始的处理函数
return(alarm(0));
}
例子2 超时读
#include "apue.h"
#include "setjmp.h"
#include <sys/wait.h>
static void sig_alarm(int);
static jmp_buf env_alrm;
int main(void)
{
int n;
char line[MAXLINE];
if (signal(SIGALRM, sig_alarm) == SIG_ERR)
err_sys("cannot catch sigalarm");
if (setjmp(env_alrm) != 0) //保存当前位置的上下文
err_quit("timeout");
alarm(10);
if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
err_sys("read error");
alarm(0);
write(STDOUT_FILENO, line, n);
exit(0);
}
static void sig_alarm(int signo) {
if (signo == SIGALRM)
printf("received %d, pid %d\n", signo, getpid());
longjmp(env_alrm, 1); //返回到setjmp处,一种异常处理方式
}
10. 9 信号集
表示一组信号。
#include <signal.h>
int sigemptyset(sigset_t *set); //清空
int sigfillset(sigset_t *set); //初始化所有信号
int sigaddset(sigset_t *set, int signo); //添加
int sigdelset(sigset_t *set, int signo); //删除
int sigismember(const sigset_t *set, int signo); //判断是否是信号集的成员
所有应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次。
如果实现的信号数目少于一个整型量所包含的位数,则可用一位代表一个信号的方法实现信号集。
10.10 sigprocmask
进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。
调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。
10.11 sigpending
sigpending函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。
例子
#include "apue.h"
static void sig_quit(int);
int main(void)
{
sigset_t newmask, oldmask, penmask;
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
err_sys("signal error");
sigemptyset(&newmask); //清空信号集
sigaddset(&newmask, SIGQUIT); //添加quit信号
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) //用newmask新增阻塞的信号集,oldmask保存原始屏蔽信号集
err_sys("sigblock error");
sleep(5); //这次期间发出quit会阻塞
if (sigpending(&penmask) < 0) //查询阻塞的信号集
err_sys("sig pending error");
if (sigismember(&penmask, SIGQUIT)) //查看sigquit是否被阻塞
printf("pending quit\n");
if (sigprocmask(SIG_SETMASK, &oldmask, NULL)) //恢复原来的屏蔽信号集
err_sys("sig setmask error");
printf("sigquit unblock\n");
sleep(5); //这期间quit可以生效
exit(0);
}
static void sig_quit(int signo) {
printf("receive sigquit\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
err_sys("sigquit error");
}
10.12 sigaction
sigaction函数的功能是检查或修改与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的不可靠的signal函数。不可靠在这里
指的是信号可能会丢失:一个信号发生了,但进程却可能一直不知道这一点,同时没办法控制阻塞信号的传递。
在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。这样在调用信号处理程序时就能阻塞某些信号。
10.13 sigsetjmp和siglongjmp
在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。在进入信号处理程序时当前信号会加入信号屏蔽字中,从longjmp中返回时不一定会恢复原始的屏蔽字。sigsetjmp和siglongjmp则能保存和恢复原始屏蔽字。
10.14 sigsuspend
sigsuspend在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这样能避免在恢复信号屏蔽字之后调用pause之前有信号到达。
例子
#include "apue.h"
#include <errno.h>
static void sig_int(int);
void pr_mask(const char* str) {
sigset_t sigset;
int errno_save;
errno_save = errno; /* we can be called by signal handlers */
if (sigprocmask(0, NULL, &sigset) < 0) {
err_ret("sigprocmask error");
} else {
printf("%s", str);
if (sigismember(&sigset, SIGINT))
printf(" SIGINT");
if (sigismember(&sigset, SIGQUIT))
printf(" SIGQUIT");
if (sigismember(&sigset, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigset, SIGALRM))
printf(" SIGALRM");
/* remaining signals can go here */
printf("\n");
}
errno = errno_save; /* restore errno */
}
int main(void)
{
sigset_t newmask, oldmask, waitmask;
pr_mask("program start");
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("error");
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) //设置阻塞int
err_sys("sigprocmask error");
pr_mask("in critical region");
if (sigsuspend(&waitmask) != -1) //设置成阻塞usr1并休眠进程,其他信号不阻塞
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend");
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) //恢复原来的屏蔽信号集
err_sys("setmask error");
pr_mask("program exit");
exit(0);
}
static void sig_int(int signo) {
pr_mask("\nint sig_int: ");
}
sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。还可以用信号实现父、子进程之间的同步,在进程控制中使用的tellwait的实现就是基于上面的方法。
int main(void)
{
pid_t pid;
TELL_WAIT(); //设置当前进程阻塞usr1, usr2信号
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
WAIT_PARENT(); /* parent goes first */ //等待信号处理程序设置全局变量
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
TELL_CHILD(pid); //向子进程发送usr1信号
}
exit(0);
}
具体实现是通过suspend阻塞等待进程,而被等待的进程发送信号触发信号处理函数修改全局变量,从而释放阻塞进程。WAIT_PARENT和TELL_CHILD是一组,TELL_PARENT和WAIT_CHILD是一组。
#include "apue.h"
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");
}
10.15 函数abort
abort函数的功能是使程序异常终止。函数将SIGABRT信号发送给调用进程。abort并不理会进程对此信号的阻塞和忽略。
太多了,以后有时间再补充。
浙公网安备 33010602011771号