进程/线程/协程(三、信号)

想不到这么快,不过进程的相关介绍也没多少内容,信号本来就是进程通信的一部分,我们就来看看所谓的信号。

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 可重入函数

如果进程在执行的时候,突然捕捉到一个信号,然后正常是跳到信号的回调函数中执行,当程序执行完成后,这时候再从信号回调函数中返回到程序中执行,这是程序如果能继续执行就说明此函数是可重入的。

下面列举几个例子来说明不可重入函数:

  1. malloc,如果在进程正在之心malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号,而跳转到信号执行函数,这样会造成什么影响?可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,这时候可能在更改此链表,这时候执行信号函数,然后再信号函数中再次malloc。就会影响到原来进程的执行。
  2. 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。

注意:

  1. UNIX系统在经过一定时间后会重新使用进程ID,就是我们判断如果这个进程存在,但是也可能是之前的进程已经挂掉了,然后重新启动一个新进程使用了这个PID。
  2. 测试进程存在的操作不是原子操作,在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

我们之前讲的函数大部份都是不对信号排队的,但是后来又了扩展添加了对信号排队的支持。

使用排队信号必须做一下几个操作:

  1. 使用sigaction函数安装信号处理程序时指定SA_SIGINFO.
  2. 在sigaction结构的sa_sigaction成员中,提供信号处理程序,
  3. 使用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)

信号一言难尽啊,第一次学习信号就到这里了,以后还是要写一些具体的代码,才能更好的熟悉信号,目前只是学习而已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值