想不到这么快,不过进程的相关介绍也没多少内容,信号本来就是进程通信的一部分,我们就来看看所谓的信号。
3.1 信号
3.1.1 信号概述
每一个信号都有一个名字,这些名字都以3个字符SIG开头。这些信号是定义在<signal.h>中,信号名都被定义为正整数常量。不存在编号为0的信号,kill函数对信号编号0有特殊应用。
信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量来判断是否发生了信号,而必须告诉内核你怎么处理这个信号。
(1)忽略此信号。大多数信号都可以使用这种方式进行处理,当有两种信号决不能被忽略。他们是SIGKILL和SIGSTOP。
(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。不能捕捉SIGKILL和SIGSTOP信号。
(3)执行系统默认动作。
下图列一下各个信号:
3.1.2 signal函数
void (*signal(int signo, void (*func)(int)(int)));
signo参数是信号名,func的值是常量SIG_IGN。常量SIG_DFL或者接受到此信号后要调用的函数地址。
实例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
static void sig_usr(int);
int main(void)
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("can't catch SIGUSR1\n");
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("can't catch SIGUSR2");
for( ; ; )
pause();
}
static void sig_usr(int signo)
{
if (signo == SIGUSR1) {
printf("received SIGUSR1\n");
} else if (signo == SIGUSR2) {
printf("received SIGUSR2\n");
} else {
printf("received error\n");
}
}
运行结果:
root@ubuntu:~#
root@ubuntu:~# ./a.out &
[1] 1528
root@ubuntu:~#
root@ubuntu:~#
root@ubuntu:~# kill -USR1 1528
received SIGUSR1
root@ubuntu:~#
root@ubuntu:~# kill -USR2 1528
received SIGUSR2
root@ubuntu:~#
root@ubuntu:~#
3.1.3 程序启动时信号的状态
当执行一个程序时,所以信号的状态都是系统默认或者忽略。通常所有信号都被设置为他们的默认动作,除非调用exec的进程忽略该信号。确切的讲,exec函数将原先设置为要捕获的信号都更改为默认动作,其他信号的状态则不变。
shell自动将后台进程对中断和退出信号的处理方式设置为忽略。
很多捕捉这两个信号的交互程序具有下列形式的代码:
void sig_init(int), sig_quit(int);
if(signal(SIGINT, SIG_IGN) !+ SIG_IGN)
signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
signal(SIGQUIT, sig_quit)
3.1.4 进程创建对信号的影响
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
3.2 信号的注意事项
信号是一个异步通信机制,虽然我们经常使用,但是也需要注意一些事项,防止编程出现问题。
3.2.1 中断系统调用
如果进程在执行一个低速系统调用而阻塞期间捕获到一个信号,则改系统调用就被中断不再继续执行。该系统调用返回错误,其error设置为EINTR。
如果有被中断系统调用相关的问题是必须要显式地处理出错返回。典型的代码如下:
again:
if(n = read(fd, buf, BUFFSIZE) < 0) {
if(error == EINTR) {
goto again;
}
}
另外:在4.2BSD引进了某些被中断系统调用的自动重启动。目前不涉及。以后有遇到再看。
3.2.2 可重入函数
如果进程在执行的时候,突然捕捉到一个信号,然后正常是跳到信号的回调函数中执行,当程序执行完成后,这时候再从信号回调函数中返回到程序中执行,这是程序如果能继续执行就说明此函数是可重入的。
下面列举几个例子来说明不可重入函数:
- malloc,如果在进程正在之心malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号,而跳转到信号执行函数,这样会造成什么影响?可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,这时候可能在更改此链表,这时候执行信号函数,然后再信号函数中再次malloc。就会影响到原来进程的执行。
- getpwam,虽然我也不知道这个函数是干啥的,但是这个函数是使用了静态存储单元中的函数,所以也是不能重入的。
下面的图中的函数是可重入函数,也被称为异步信号安全的,除了可重入外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。
注意:每个线程只有一个errno变量,所以信号处理程序可能会修改其原先值,应当在调用前保存errno,在调用后恢复errno。(信号函数中如果使用free,可能主函数也使用free,这样就会冲突,所以正常做法就是,free完后,把指针置NULL,然后下次free时候,判断一下指针)
3.3 信号函数
3.3.1 kill和raise
int kill(pid_t pid, int signo); //将信号发送给进程或进程组,进程使用的发送信号的函数
int raise(int signo); //允许进程向自身发送信号
rasie(signo); 等价于:kill(getpid(), signo)
3.3.1.1 kill的参数
kill的pid参数:
pid >0 将信号发送给进程ID为pid的进程
pid = 0 将该信号发送给与发送进程属于同一进程组的所有进程。
pid < 0 将该信号发送给其他进程组ID等于pid绝对值
pid = -1 将该信号发送给发送进程由权限向他们发送信号的所有进程。
3.3.1.2 判断进程是否存在
信号编码0定义为空信号,如果signo参数是0,则kill仍执行政策的错误检查,但不发送信号,这常被用来确定一个特定进程是否仍然存在。如果向一个并不存在的进程发送空信号,则kill返回-1,errno被设置为ESRCH。
注意:
- UNIX系统在经过一定时间后会重新使用进程ID,就是我们判断如果这个进程存在,但是也可能是之前的进程已经挂掉了,然后重新启动一个新进程使用了这个PID。
- 测试进程存在的操作不是原子操作,在kill向调用者返回测试结果时,原来已存在的被测试进程此时可能已经终止。
3.3.2 alarm和pause
unsigned int alarm(unsigned int seconds); //设置一个定时器,当定时器超时时,会产生SIGALRM信号
如果忽略或不捕捉次信号,则其默认动作是终止调用该alarm函数进程。
用法:
seconds是产生信号SIGALRM需要经过的时钟秒数,当这一时刻到达时,信号由内核产生。
second > 0 | 如果在之前已经注册了闹钟还没超时,该闹钟的余留值做为本次alarm调用值返回,以前注册的闹钟则被新值代替 |
second = 0 | 如果以前注册的尚未超时的闹钟,会取消以前的闹钟,余留值还是做为函数返回 |
int pause(void); //pause函数使调用进程挂起直至捕捉到一个信号
可以用alarm和pause实现sleep函数,不过这种sleep函数缺点很多,就不描述了
3.4 信号集
3.4.1 信号集函数
我们需要有一个能表示多个信号–信号集的数据类型。以便告诉内核不允许发生该信号集中的信号。
定义的数据类型是sigset_t以包含一个信号集,下面是处理的函数
int sigemptyset(sigset_t *set); //初始化由set指向的信号集,清楚其中所有信号
int sigfillset(sigset_t *set); //初始化由set指向的信号集,使其包括所有信号
int sigaddset(sigset_t *set, int signo); //将一个信号添加到已有的信号集中
int sigdelset(sigset_t *set, int signo); //将从信号集中删除一个信号
int sigismember(const sigset_t *set, int signo); //测试一个信号是否开启
3.4.2 sigprocmask
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//检测和更改一个进程的信号屏蔽字,屏蔽字是规定了这个信号不能传递给进程(阻塞信号)
参数说明:
how | 说明 |
---|---|
SIG_BLOCK | 新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了希望阻塞的信号 |
SIG_UNBLOCK | 新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的补集。set包含了解除阻塞信号 |
SIG_SETMASK | 新的信号屏蔽是set指向的值 |
3.4.3 sigpending
int sigpending(sigset_t *set);
//获取当前被屏蔽的信号
3.5 新的信号函数
3.5.1 sigaction
int sigaction(int signo, const strcut sigaction *restrict act, struct sigaction *restrict oact);
//绑定信号的,替代signal函数
这个函数比较常用,是替代以前的signal函数的。
struct sigaction {
void (*sa_handler)(int); //信号回调函数
sigset_t sa_mask; //信号集
int sa_flag; //信号选择
void (*sa_sigaction)(int, siginfo_t *, void *); //备用回调函数
}
3.5.1.1 参数说明
sa_flag字段指定对信号进行处理的各个选项
选项 | |
---|---|
SA_INTERRUPT | 信号中断的系统调用不自动重启 |
SA_NOCLDSTOP | |
SA_NOCLDWAIT | |
SA_NODEFER | |
SA_ONSTACK | |
SA_RESETHAND | |
SA_RESTART | 信号中断的系统调用自动重启动 |
SA_SIGINFO | 此选项对信号处理程序提供了附加信息 |
sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。
对于sa_sigaction字段和sa_handler字段两者,实现使用同一存储区,所以应用只能一次使用这两个字段中的一个。
siginfo结构包含了信号产生原因的有关信息。
struct siginfo {
int si_signo;
int si_errno;
int si_code;
pid_t si_pid;
uid_t si_uid;
void *si_addr;
int si_status;
union sigval si_value;
union sigval si_value;
}
union sigval {
int sival_init;
void *sival_ptr;
}
3.5.2 实例:signal函数
Sigfunc * signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
act.sa_flags |= SA_RESTART;
}
if(sigaction(sugno, &act, &oact) < 0) {
return(SIG_ERR);
}
return(oact.sa_handler);
}
看着就是用sigaction函数替换了原来的signal。
3.5.2 sigsetjmp和siglongjmp
跳过
3.5.3 sigsuspend
int sigsuspend(const sigset_t *sigmask);
进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且 从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
目前还不知道用在什么地方,以后用到在回来补上。
3.6 其他信号函数
3.6.1 abort
int abort(void);
此函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。
abort函数的实现:
void abort(void) {
sigset_t mask;
struct sigaction action;
sigaction(SIGABRT, NULL, &action); //获取之前绑定的SIGABRT
if(action.sa_handler == SIG_IGN) {
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL); //新绑定的处理函数
}
if(action.sa_handler == SIG_DFL)
fflush(NULL);
sigfillset($mask);
sigdelset(&mask, SIGABRT);
sigprocmask(SIG_SETMASK, &mask, NULL); //阻塞其他信号,只保留了SIGABRT
kill(getpid(), SIGABRT);
fflush(NULL);
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
sigprocmask(SIG_SETMASK, &mask, NULL);
kill(getpid(), SIGABRT);
exit(1);
}
这个程序不是看的很懂的意思,难受,先放一放把,信号这玩意确实难。
3.6.2 system
之前写的system函数,是不考虑到信号,今天一看信号,原来发现信号这么难。所以这里实现一个处理信号的system函数。
实例:
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
int system(const char *cndstring)
{
pid_t pid;
int status;
struct sigaction ignor, saveintr, savequit;
sigset_t chldmask, savemask;
if(cmdstring == NULL)
return(1);
ignore.sa_handler = SIG_IGN;
sigemptyset(&ignore.sa_mask); //初始化信号
ignore.sa_flags = 0;
if(sigaction(SIGINT, &ignore, &saveintr) < 0) { //绑定int信号
return -1;
}
if(sigaction(SIGQUIT, &ignore, &savequit) < 0) {
return -1;
}
sigemptyset(&chldmask); //子进程的信号
sigaddset(&chldmask, SIGCHLD);
if(sigprocmask(SIG_BLOCK, &chldmask, &savemask))
return -1;
if((pid = fork()) < 0) {
status = -1;
} else if(pid == 0) { //子进程
sigaction(SIGINT, &saveintr, NULL); //恢复进程开始的信号处理操作
sigaction(SIGQUIT, &savequit, NULL);
sigprocmask(SIG_BLOCK, &savemask, NULL);
execl("/bin/sh", "sh", '-c', cmdstring, (char *)0);
_exit(127);
} else {
while(waitpid(pid, &status, 0)) {
if(errno != EINTR) {
status = -1;
break;
}
}
}
//恢复之前的进程操作
if(sigaction(SIGINT, &saveintr, NULL) < 0)
return -1;
if(sigaction(SIGQUIT, &savequit, NULL) < 0)
return -1;
if(sigprocmask(SIG_SETMASK, &savemask, NULL))
return -1;
return status;
}
有了信号处理部分还是复杂了很多,其实写了几个,对sigaction(SIGINT, &ignore, &saveintr)这个函数有所了解了,SIGINT毋庸置疑就是信号,然后ignore第二个参数,就是我们要修改的信号集,如果为空的话,就不操作,saveintr第三个参数,其实就是在绑定这个之前,SIGINT的信号状态,做保存作用的,也可以做为恢复作用,看到最后就明白了,在恢复几个信号。
3.6.3 sleep、nanosleep、clock_nanosleep
unsigned int sleep(unsigned int seconds);
此函数使调用进程被挂起直到满足下面两个条件之一
(1)已经过了seconds所指定的墙上时钟时间
(2)调用进程捕捉到一个信号并从信号处理程序返回
sleep函数实例:(避免了早期实现的竞争状态,但是仍未处理与以前的闹钟的交互作用)
static void sig_alarm(int signo) {}
unsigned int sleep(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmsak, oldmask, suspmask;
unsigned int unslept;
//设置输出值,保存旧的配置
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
sigaction(SIGALRM, &newactm &oldact);
//阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmsk(SIG_BLOCK, &newmask, &oldmask);
alarm(seconds); //启动alarm,准备过了second之后唤醒
suspmask = oldmask;
//不阻塞SIGALRM信号
sigdelset(&suspmak, SIGALRM);
sigsuspend(&suspmask); //等待信号被捕捉
//这是唤醒之后了
unslept = alarm(0); //取消alarm
//恢复信号
sigaction(SIGALRM, &oldact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}
只能说能看懂程序的意思,确不能了解深入的意义,信号确实有点难啊,第一次接触信号是这样,等以后还有机会实际应用应用就会知道了。
int nanpsleep(const struct timespec *reqtp, strcut timespec *remtp);
跟sleep一样,但是提供了纳秒的延时
int clock_nanosleep(clockid_t clock_id, int flags, const strcut timespec *reqtp, struct timespec *remtp);
需要使用相对于特定时钟的延迟时间来挂起调用线程。
clock_id参数指定了计算延迟时间基于的时钟。
flags参数用于控制延时是相对的还是绝对的。flags为0表示休眠时间是相对的。
3.6.4 sigqueue
我们之前讲的函数大部份都是不对信号排队的,但是后来又了扩展添加了对信号排队的支持。
使用排队信号必须做一下几个操作:
- 使用sigaction函数安装信号处理程序时指定SA_SIGINFO.
- 在sigaction结构的sa_sigaction成员中,提供信号处理程序,
- 使用sigqueue函数发送信号
int sigqueue(pid_t pid, int signo, const union sigval value);
信号不能给无限排队,有一个SIGQUEUE_MAX的限制。到达相应的限制以后,sigqueue就会失败,将errno设为EAGAIN。
3.6.5 psiginfo
如果在sigaction信号处理程序中有siginfo结构,可以使用psiginfo函数打印信号信息
void psiginfo(const siginfo_t *info, const char *msg)
信号一言难尽啊,第一次学习信号就到这里了,以后还是要写一些具体的代码,才能更好的熟悉信号,目前只是学习而已。