Linux系统编程05--信号2

文章目录

    • 五、信号-2
      • 进程处理信号的行为
        • PCB信号集
          • 信号集处理函数
          • sigprocmask信号屏蔽字函数
          • sigpending获取当前信号集的未决信号集
        • 信号捕捉设定
        • 用户自定义信号(利用SIGUSR1和SIGUSR2实现父子进程同步输出)
        • C标准库信号处理函数
        • 可重入函数
        • 信号引起的竞态和异步I/O
          • 时序竞态(进程竞争CPU资源)
        • 避免异步I/O的类型
          • volatile
        • SIGCHLD信号
          • SIGCHLD信号产生条件
        • 向信号捕捉函数传参
          • sigqueue
          • sigaction中第二个函数原型
        • 信号中断系统调用

五、信号-2

进程处理信号的行为

进程处理信号有三种行为,在manpage里信号3种处理方式:

1.SIG_DFL	默认信号处理
2.SIG_IGN	忽略信号
3.a signal handling function	自定义信号处理行为(通过自定义函数来反馈信号)

即如下:进程处理信号的行为

1.默认处理动作(每个信号都有一个默认处理动作,如果当前进程没有设置忽略或者捕捉信号,则当收到信号时进行默认处理动作)
默认处理有5种动作(Action)情况:
	Term	终止进程(terminate)
	Core	终止进程,并且生成core文件,转储核心(储存终止进程之前其内存情况,验尸),如下
		gcc -g file.c
		ulimit -c 1024
		gdb a.out core
		进程死之前的内存情况,死后验尸
	Ign		忽略该信号,即什么都不做,其与下面的忽略行为效果一样,但是层级不一样。
	Stop	暂停进程
	Cont	继续进程执行
2.忽略
3.捕捉(用户自定义信号处理函数)有点类似中断,当中断触发时会去执行中断处理函数

现在5种默认信号处理行为改为了A、B、C、D、E、F,代表含义一样

       "动作(Action)"栏 的 字母 有 下列 含义:

       A      缺省动作是结束(terminate终止)进程.
       B      缺省动作是忽略这个信号.
       C      缺省动作是结束进程, 并且核心转储.
       D      缺省动作是停止(stop暂停并非终止)进程.
       E      信号不能被捕获.
       F      信号不能被忽略.
       
       (译注: 这里 "结束" 指 进程 终止 并 释放资源, "停止" 指 进程 停止 运行, 但是 资源
       没有 释放, 有可能 继续 运行.)
PCB信号集


​ 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

​ 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

​ 如图,当进程接收到信号后,进程pcb内维护着两个信号集,未决信号集、阻塞信号集。

  • 未决信号集:每一个信号编号对应一个位,默认时候都为1,当进程接收到信号,进程pcb将信号传给未决信号集,对应信号位置1,表示进程此刻接收到了信号,但没有进行响应,此刻标记该信号为未决态。

  • 阻塞信号集(也叫信号屏蔽字):当进程接收到信号,标记信号为未决态后,未决信号集将信号传给阻塞信号集,如果其阻塞位为1,表示该进程阻塞该信号,拒绝进行响应,不做出行为活动,此时,该信号一直被标记为未决态;如若阻塞位为0,则该信号通过阻塞信号集,并根据设定进行响应,并且内核将前面该信号的未决位由1该为0,此刻信号为递达态。

    需要注意的是:

  • 未决信号集是内核控制的,进程接到信号就对应未决位置1(未响应),当进程对该信号进行响应后,其对应未决位由内核自动置0,用户不能设置,但可以查询;阻塞信号集用户可以设置,用来控制进程对相应信号进行阻塞不响应。

  • 未决位当该信号通过阻塞,进入行为的同时,信号就已经从未决态变为递达态(递达即递达到执行行为阶段,信号一通过屏蔽字,其未决位就被内核置0了,此时捕捉函数或者动作刚刚开始),故如果标准函数或者行为用时长比如5秒时间,那么在这5秒开始的那一刻,该信号就变为递达态,进程的该信号未决位置0,迎接下一个信号,所以在上一个信号进入捕捉函数执行阶段(5秒内),进程可以接收信号。但是前32位信号未决位只能记录一个,多个重复信号都是只记录一个。

  • handler为前面讲的行为:1.默认;2.忽略;3.捕捉。每一个信号都对应一个handler。

  • 为了保证系统安全,并不是所有信号都可以阻塞的,系统会保留几个必要信号(SIGQUIT、SIGTSTP)不可阻塞,留给系统管理使用,如果不这样设定的话,那么用户将所有信号都阻塞了,那内核岂不是不能控制该进程了。

  • 前32个信号为Linux经典信号,不支持排队;后32个信号为实时信号,支持排队;

    • 前32个信号,比如,进程收到信号2收到了10次,但是阻塞了,未决态不会记录有10次信号未决,而是不管收到几次都当作一次信号。
    • 后32个信号是实时信号,常用于硬件驱动等等需要实时信号响应的场景,需要记录每一个重复信号并且排队处理,这时每个信号都是有自己含义的,比如鼠标的单击和快速双击的结果是不同的。
信号集处理函数
sigset_t为信号集,可sizeof(sigset_t)察看

int sigemptyset(sigset_t *set)  清空信号集(模板)
int sigfillset(sigset_t *set)   全部置1
int sigaddset(sigset_t *set, int signo)   把该信号集(模板)某一个信号位置1
int sigdelset(sigset_t *set, int signo)	  把该信号集(模板)某一个信号位置0
int sigismember(const sigset_t *set, int signo)	判断该信号集(模板)的某一个信号位是否为1
参数:
    sigset_t表示信号集(模板),signo表示信号编号
return:
    sigemptyset(),sigfillset(),sigaddset(),sigselset()成功返回0,失败返回-1
    sigismember()是查询信号集某位是否为1,如若为1则返回1,如若不为1返回0;

​ 信号集处理函数处理的对象不是pcb内的具体信号集,而是先自己构建一个信号集sigset_t作为模板,运用上述信号集函数将需设定的信号集内容填入到模板内,再将完成的模板粘贴应用到相应进程PCB内。

sigset_t信号集:信号集被定义为一种数据类型:
  typedef struct {
  unsigned long sig[_NSIG_WORDS];
  } sigset_t

sigprocmask信号屏蔽字函数

调用函数sigprocmask可以读取或更改进程的信号屏蔽字。

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
    1.how:选择执行操作类型,三种执行情况
    2.set:信号集指针,指向自己构建的信号集模板,作为传入参数
    3.oset:信号集指针,指向信号集指针buf,作为传出参数,函数将此刻屏蔽字(设置前)传出来,保存到oset内。若不关心原屏蔽字直接NULL
return:若成功则为0,若出错则为-1

​ 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

how参数的含义:

SIG_BLOCK    信号集(模板)set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set,将模板与其原有的屏蔽字进行|操作
SIG_UNBLOCK  信号集(模板)set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set,将模板与原有屏蔽字进行&~操作
SIG_SETMASK  设置当前信号屏蔽字为set所指向的值,相当于mask=set,将模板替换原有屏蔽字

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending获取当前信号集的未决信号集
#include 
int sigpending(sigset_t *set);
参数:
	set:为信号集(模板)指针,传出接收参数,为传出当前进程的未决信号集储存位置,接收当前进程的未决信号集。

sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

#include 
#include 
void printsigset(const sigset_t *set)
{
	int i;
	for (i = 1; i < 32; i++)
		if (sigismember(set, i) == 1)
			putchar('1');
		else
			putchar('0');
	puts("");
}

int main(void)
{
	sigset_t s, p;
	sigemptyset(&s);
	sigaddset(&s, SIGINT);
	sigprocmask(SIG_BLOCK, &s, NULL);
	while (1)
    {
		sigpending(&p);
		printsigset(&p);
		sleep(1);
	}
	return 0;
}

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

xingwenpeng@ubuntu:~$ ./a.out
0000000000000000000000000000000
0000000000000000000000000000000(这时按Ctrl-C)
0100000000000000000000000000000
0100000000000000000000000000000(这时按Ctrl-\)
Quit (core dumped)

信号捕捉设定
#include 
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

结构体struct sigaction 定义:
	struct sigaction {
	void (*sa_handler)(int);捕捉函数指针(老版本),设定捕捉函数,按函数接口格式设定捕捉函数
	void (*sa_sigaction)(int, siginfo_t *, void *);捕捉函数指针(新版本),设定捕捉函数,和上面的作用一样,只是函数接口更加强大灵活,与上面互斥
	sigset_t sa_mask;设定捕捉函数执行过程中的信号屏蔽字,因为当该信号设定捕捉函数时,若进程收到其他信号,会造成信号嵌套,逻辑混乱,所以当该信号捕捉函数执行时,屏蔽掉其他信号,当该信号捕捉函数执行完后恢复为原来的屏蔽字。
	int sa_flags;选择捕捉函数接口,即在第一个成员和第二个成员之间选择捕捉函数接口格式,只能选择一种(互斥)
	void (*sa_restorer)(void);不再使用
	};
	成员:
	sa_handler : 早期的捕捉函数
	sa_sigaction : 新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数
	sa_mask : 在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
	阻塞信号集
	sa_flags : SA_SIGINFO(第二个函数原型) 或者 0(默认函数即第一个函数)
	sa_restorer : 保留,已过时
参数:
	signum:信号编号,需要设置捕捉函数的信号编号或宏名
	act:信号动作函数结构体,输入参数,将自己构建的信号动作结构体传给捕捉设定函数,
	oldact:信号动作函数结构体,输出函数,将原本信号捕捉函数结构体输出出来,若不关心原本捕捉函数则直接用NULL。
SIG_DFL、SIG_IGN

需要注意的是:

  • 信号的捕捉函数只能有一个,即信号的响应动作只能有一个,sigaction结构体内有两个捕捉函数指针,前一个是早期Linux的捕捉函数指针,接口比较简单,所以后期补充了第二个捕捉函数指针,这两个捕捉函数指针性质完全一样,只是新加的捕捉函数的接口更加丰富灵活,他们两个是互斥的,只能挑选一个作为信号的捕捉函数。
  • 信号捕捉函数设定函数中,结构体sigaction内的sa_mask成员是设定当该信号响应并执行捕捉函数行为时暂时设定的该进程信号屏蔽字,为了避免在该信号调用捕捉函数时进程接收到其他信号,如果进程还对新信号进行响应的话会造成信号嵌套,逻辑混乱,所以在设定捕捉函数时设定一个临时的屏蔽字,阻塞新信号响应,当该信号的捕捉函数执行完后,恢复为原有屏蔽字,其中mask = mask | sa_mask,并不是直接用sa_mask覆盖原来的mask,而是取或操作,并且为了确保上一个信号安全的执行捕捉函数,在上一个信号执行捕捉函数过程中,内核自动将正在执行捕捉函数的信号屏蔽字设为阻塞,防止下一个信号通过屏蔽字造成混乱,当该信号动作完成后,屏蔽字自动恢复,让下一个未决信号通过并进行动作。
  • 成员sa_handler的值为三种,即上图中handler中的三种:1.可以是函数指针(捕捉函数指针);2.也可以是默认动作,SIG_DFL;3.也可以是忽略信号,SIG_IGN。
    Linux系统编程05--信号2_第1张图片

​ 上图体现的是内核处理信号的流程,并不是信号一来就响应,而是在一定情况下去处理信号,当然对于高速运行的计算机,这种情况(系统转变为内核态处理异常或者中断)几乎时时在发生,所以在宏观上给人的感觉就是立即响应信号,其实并不是。比如主函数在用户空间执行一条语句如i++,它花费10MS,在这10MS内进程接收到了一个信号,进程不会中断i++去响应信号,而是进行执行i++完毕,再执行下一个(用户空间)语句,只有当进程调用系统调用、处理中断、异常时,操作系统进入内核态,比如进程A执行完一个时间片后,系统发出定时中断,进程A由于已经运行了一个时间片,内核剥夺进程A的cpu资源操作系统回归到内核态,发现进程A有一个未决信号,此刻也不会去立即响应进程A的信号,因为进程A 已经已完了当前的时间片,根据进程调度,操作系统将cpu资源分配给其他进程,当下一次轮到进程A使用CPU资源,此刻系统在内核态,准备进入进程A(用户空间的主程序时)发现上次的信号还在未决态,然后内核就直接进入信号处理,处理完后才进入用户空间的进程A主函数执行主函数。这里强调的是,进程和内核不是第一时间就响应接收到的信号的,而是在进程运行或者操作系统运行过程中,有无数个进入内核态再进入用户态是事件,信号统一在这个事件发生时处理。

信号与中断很像,但是不是中断,宏观处理方式很像,但是本质不同,中断是立即响应,信号并不是立即响应,是在特定时间才响应。

信号的具体响应:

主函数:

int main()
{
    struct sigaction act;
    int i;
    act.sa_handler = act_i;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,NULL);
    while(1)
    {
        write(STDOUT_FILENO,“* * * * * * * * * *\n",20);
        sleep(1);
    }
    return 0;
}

捕捉函数1:

void act01(int num)
{
    int i = 1;
    printf ("i am act01\n");
    while(i){
    printf("num = %d\n",num);
    sleep(1);
    i--;
    }
}

捕捉函数2:

void act02(int ac)
{
    printf("ac = %d\n", ac);
}

结果1:

jiaojian@KyLin:~/Linux/APUE/signal$ ./act02
* * * * * * * * * *
* * * * * * * * * *
* * * * * * * * * *
^Ci am act01
num = 2
^C^Ci am act01
num = 2
^C^C^C^Ci am act01
num = 2
^C^C^C^C^C^Ci am act01
num = 2
^C^C^C^Ci am act01
num = 2
* * * * * * * * * *
* * * * * * * * * *
^Ci am act01
num = 2
* * * * * * * * * *
* * * * * * * * * *
* * * * * * * * * *
^\退出 (核心已转储)

结果2:

jiaojian@KyLin:~/Linux/APUE/signal$ ./act
* * * * * * * * * *
* * * * * * * * * *
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^Cac = 2
* * * * * * * * * *
^\退出 (核心已转储)

从上面的两种结果可以发现。第二种捕捉函数,不管你多快的发送信号都不可连续两次及以上的执行捕捉函数(即接收一连串的信号)。

  • 首先,理解未决态,在进程接收到信息,未决信号集相应信号为置1,接着传给屏蔽字,若屏蔽字堵塞,则未决位保持1,信号一直处于未决态;若屏蔽字不堵塞,则进程响应信号,进而去执行行为活动(默认、忽略、捕捉函数),此刻一通过屏蔽字,内核自动将该信号设为递达态,未决位置0(并不是要等到响应活动执行完后才去除未决态,为啥响应活动开始的那一刻,未决态转变为递达态);
  • 捕捉函数1,当进程收到SIGINT信号(此时用户空间的主函数已经暂停,系统在内核空间运行),pcb将2信号传给未决信号集。2信号未决位置1,接着查看屏蔽字,2信号屏蔽字为0,此刻2信号通过屏蔽字,此刻内核将未决位置0,信号由未决态转变为递达态,进入handler,此时该信号行为为捕捉函数,执行捕捉函数act01,执行act01过程有sleep(1)即执行该捕捉函数要花费1S多时间,那么在执行捕捉函数期间,如若此刻进程接收到2信号,那么新的2信号就会被传到未决信号集,2信号未决位置1,但是由于此刻上一个2信号正在执行捕捉函数,为了保护上一个信号执行捕捉函数,2信号屏蔽字为阻塞,当上一个信号动作完成后内核开放2信号屏蔽字,然后内核发现2信号有未决信号,则没有回到用户空间的主函数上,而是处理新来的未决信号,此时上一个信号完成了动作,屏蔽字开放了,新信号直接通过屏蔽字,进入动作,内核又将未决位置0;所以可以一连串向进程发送2信号,让他时刻有未决信号,当然,前面说到,前32个信号没有排队机制,快速一连串的未决信号对于进程来说都是一个未决信号。
  • 捕捉函数2,当进程接收到SIGINT信号,pcb将信号传给未决信号集,2信号未决位置1,查看屏蔽字,不阻塞,进入捕捉函数(此刻信号由未决转变为递达态),该进程可以接收下一个信号,但是相比于捕捉函数1执行需要1S时间,函数2基本上一个时间片就完成了,非常快,人手动按键传入信号根本跟不上函数2执行,所以,函数2在很快时间执行完就跳出捕捉函数回归主函数,而主函数此时在循环内,每次循环1S,所以此刻若收到信号再跳入信号处理捕捉函数,才会出现想上面的结果。故是由于捕捉函数过快,所以按键无法在捕捉函数极短的执行时间内发信号,从而像捕捉函数1一样连续的执行捕捉函数。
  • 前32个信号不支持队列排队,即在前一个信号再执行捕捉函数期间,若进程接收到好多个相同信号,此时未决位置1,但是这多个信号对应进程来说还是相当于一个信号,没有像实时信号一样每一个信号都要响应,这里准确的来说是多个“未决“信号都被看为一个未决信号。
  • 捕捉函数是内核去调取的,handler捕捉函数有一个int参数,如上面捕捉函数1的num,为什么是2呢?其实就是该信号编号。
  • 在编写程序时任意搞混淆的事情是不管是信号集函数还是捕捉设定函数使用的参数都是模板粘贴,并不是直接更改进程的信号集

函数总结:

sigset_t模板编辑函数:

  • sigemptyset(sigset_t *set),将sigset_t模板所有位置清空为0,empty为清空的意思。
  • sigfillset(sigset_t *set),将sigset_t模板的所有信号位置1.fill填满的意思。
  • sigaddset(sigset_t *set, int sig),将sig信号位置1.与sigdelset()是相反操作。
  • sigdelset(sigset_t *set, int sig),将sig信号位置0,一般是编辑屏蔽字,因为未决信号集用户是无法控制的。
  • sigismember(sigset_t *set, int sig),查询信号sig是否为1(为模板查询,需把屏蔽字或者未决信号集用下面的查询复制函数复制到准备好的模板sigset_t内,再用此函数查询具体信号编号是否为1),若为1返回1,为0返回0;

sigset_t模板写入与查询函数:

  • sigprocmask(int how, const sigset_t *set, sigset_t *oset),how为三种模式选择,set为输入的模板指针(输入参数),oset为接收原信号的屏蔽字模板指针(输出接收参数)。
  • sigpending(sigset_ t *set),set为接收原未决信号集模板指针(输出接收参数),该函数只有查询功能,查询当前时刻的未决信号有哪些,可以结合sigismember()函数,用户不可设置未决信号集。

需要注意的是以上所有函数参数中都是用的模板指针sigset_t*和sigsction*

用户自定义信号(利用SIGUSR1和SIGUSR2实现父子进程同步输出)

10:SIGUSR1和12:SIGUSR2内核没有定义其功能,让用户自己去定义他们的功能,称为用户自定义信号,系统设置的默认动作为终止,用户可以根据自己需求修改这两个信号的捕捉函数动作。

kill -SIGUSR1 6978,这段命令是向pid为6978的进程发送SIGUSR1信号。

注意:子进程继承了父进程的信号屏蔽字和信号处理动作,因为子进程是完全继承了父进程的pcb(除了pid)

C标准库信号处理函数

前面介绍的sigaction()函数是Linux提供的信号处理函数,只能在Linux系统下使用,c标准函数可以在win和Linux下都可以使用。

typedef void (*sighandler_t)(int) 定义sighandler_t,为函数指针类型
sighandler_t signal(int signum, sighandler_t handler)
[参数]
signum为信号编号,handler为函数指针,与sigaction内的sa_hansler一模一样,signal函数比sigaction()函数简单,功能小,不可设定捕捉函数执行时的屏蔽字。

int system(const char *command)
集合fork,exec,wait一体
[参数]
command为指令的字符串,如“ls   /home”
如:设捕捉函数为do_sig(),那么在函数中用signal()函数设定SIGINT信号捕捉函数
signal(SIGINT, do_sig);比sigaction简单很多。
可重入函数

Linux系统编程05--信号2_第2张图片

如图为队列插入node1,insert()函数为插入函数,即将node1的地址指向原有队列的head,head指向node1这样完成插入,但是如果在insert()函数执行一半,node1尾部指向原有队列后,在head指向node前,突然来了一个信号,信号的捕捉函数为插入一个node2到原有的队列,insert(node2),这样就会导致在捕捉函数内node2插入后,head指向node2,捕捉函数完成,回到主函数,主函数此时为插入node1的第二步骤,将head指向node1,这样就要会有bug,node2没有真正的插入到队列,所以insert()函数为不可重入函数。

  • 不含全局变量和静态变量是可重入函数的一个要素,为了保证可重入,函数内不能有全局变量和静态变量。
  • 可重入函数见man 7 signal
  • 在信号捕捉函数里应使用可重入函数
  • 在信号捕捉函数里禁止调用不可重入函数

例如:strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱,应用strtok_r版本,r表示可重入。

多进程可以用不可重入函数,因为各进程之间资源不共享,内存空间是互相不可见的。对线程则不行,要用可重入函数,因为多线程之间资源是共享可见的。

信号引起的竞态和异步I/O
时序竞态(进程竞争CPU资源)
int pause(void)
使调用进程挂起,直到有信号递达,如果递达信号动作是忽略,则继续挂起。
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃 CPU)直到有信号递达将其唤醒

[返回值]
如果信号的默认处理动作是终止进程,则进程终止,pause 函数没有机会返回
如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause 函数不返回
如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause 函数返回-1,errno 设置为 EINTR,表示 “被信号中断”。想想我们哪个函数只有出错返回值(exec)
pause 收到的信号不能被屏蔽,如果被屏蔽,那么 pause 就不能被唤醒。

​ 时序竞态指的是在进程中如果有计数操作的函数或者模块,比如进程A内有一句alarm(1)计时一秒,当进程A执行完这一句后,CPU资源被其他进程夺去,过了2S才轮到进程A使用CPU,那么此时执行alarm(1)的下一句pause(),此时alarm(1)已经过去2S了,SIGALRM信号在一秒前已经传给进程A了,但那时进程A在就绪态,pause()没有收到信号,导致进程A一直挂起。这种由于进程内含有时序和进程竞争导致的执行错误现象就是时序竞态。

  • 产生原因:仍然以前文实现的sleep函数为例,如果进程在执行完alarm函数后,突然失去CPU,被阻塞等待(这是有可能的,进程在执行过程中,若非原子操作,都有可能随时失去CPU),如果失去CPU的时间大于了sleep函数需要睡眠的时间,则此时在执行pause函数前,信号已经到了,因此会先处理信号(软中断,而不是先执行pause函数),在信号处理完后,再去执行pause函数,此时进程会被永远挂起,不会被唤醒,因为SIGALRM信号已经被处理了。
  • 时序竞态:即由于进程之间执行的顺序不同,导致同一个进程多次运行后产生了不同结果的现象。如上述sleep函数,有时执行结果是正确的,有时却会导致进程永远被挂起,因此这就是一个时序竞态问题。因此需要重新对该函数进行改进。

为了解决上面的问题,引入通过信号屏蔽来实现SIGALRM信号返回时如果此刻pause()没有在执行,则该信号屏蔽;当pause()执行时信号没有阻塞。即

1.阻塞SIGALRM

2.执行alarm(n)函数

3.执行pause(),并且放开SIGALRM的阻塞,sigsuspend()函数就是这两步的封装

上面的三步可解决pause()函数的时序竞态问题,保证pause()可以接收到信号(通过阻塞)

int sigsuspend(const sigset_t *mask)
sigsuapend()执行的步骤:
1.以通过指定mask来临时解除对某个信号的屏蔽,这里需要在sigsuspend()函数执行前,该信号就被屏蔽了。
2.然后挂起等待
3.当被信号唤醒sigsuspend返回时,进程的信号屏蔽字恢复为原来的值
注意;sigsuspend()函数只是解除阻塞和pause()的封装,在执行sigsuspend()前就需要完成相应信号的阻塞

使用pause()函数导致出现问题的版本:

#include 
#include 
#include 
void sig_alrm(int signo)
{
	/* nothing to do */
}
unsigned int mysleep(unsigned int nsecs)
{
	struct sigaction newact, oldact;
	unsigned int unslept;
	newact.sa_handler = sig_alrm;
	sigemptyset(&newact.sa_mask);
	newact.sa_flags = 0;
	sigaction(SIGALRM, &newact, &oldact);
	alarm(nsecs);
	pause();
	unslept = alarm(0);
	sigaction(SIGALRM, &oldact, NULL);
	return unslept;
}
int main(void)
{
	while(1){
		mysleep(2);
		printf("Two seconds passed\n");
	}
	return 0;
}

mysleep改进版,通过屏蔽SIGSLRM信号,然后在挂起的同时解除屏蔽(即sigsuspend()函数是将pause()函数和解除屏蔽字封装成一个原子操作),这样可以保证alarm的信号在到达sigsuspend()前是未决的。

unsigned int mysleep(unsigned int nsecs) //该函数的返回值为未睡够的时间
{
	struct sigaction newact, oldact;
	sigset_t newmask, oldmask, suspmask;
	unsigned int unslept;
	/* set our handler, save previous information */
	newact.sa_handler = sig_alrm;//这里必须设置捕捉函数,
    //如果是SIG_DLF(默认行为为终止)那么直接就终止进程了,如果是SIG_IGN,后面的挂起就不会停止,继续挂起
	sigemptyset(&newact.sa_mask);
	newact.sa_flags = 0;
	sigaction(SIGALRM, &newact, &oldact);
	/* block SIGALRM and save current signal mask */
   //第一步:这里先阻塞SIGALRM
	sigemptyset(&newmask);
	sigaddset(&newmask, SIGALRM);
	sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    //第二步:阻塞SIGALRM设置完后再计时
	alarm(nsecs);
    
	suspmask = oldmask;
	sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */
    //第三步:通过sigsuspend()函数,进入pause()挂起,并且同时解除阻塞,即将这两步和成一步
	sigsuspend(&suspmask); /* wait for any signal to be caught */
	/* some signal has been caught, SIGALRM is now blocked */
	unslept = alarm(0);
	sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
	/* reset signal mask, which unblocks SIGALRM */
	sigprocmask(SIG_SETMASK, &oldmask, NULL);
	return(unslept);
}

sigsuspend()函数通过整合pause和信号屏蔽来解决了时序竞态问题,保证了信号会传递给sigsuspend()函数,解除挂起。

避免异步I/O的类型
sig_atomic_t
平台下的原子类型,即为了避免进程运行操作数据时,信号插入导致数据处理一半,信号捕捉函数内又对该数据进行处理,导致异步
如在32位机上的long long类型,

如在32位机上的long long类型,就不是一个原子类型,因为32位机一次能处理的最大位数为32位4个字节,long long 为8个字节,所以需要两步汇编才能对其进行操作,所以long long 对于这台机器就不是一个原子类型,而int才是一次可以操作的类型。

如:long long n = ?,给全局变量n赋值,然后进程A对n进行操作,操作在汇编机器层面分为前4个字节和后4个字节,如果进程A在操作完前4个字节后,收到一个信号sig,转而去处理sig捕捉函数,而捕捉函数也是操作n(全局变量),操作完后,回到主函数,接着处理前面没处理完的后4位,这样就导致异步,所以引入原子类型概念来避免该现象(通过typedef实现,可以具备跨平台能力)。

volatile
volatile
防止编译器开启优化选项时,优化对内存的读写

volatile与const一样都是修饰变量的,const修饰的变量为只读变量,volatile修饰的变量意思是防止编译器对该变量的优化操作,即编译器在编译源码时会进行优化操作,内存中的n只读一次,后面代码使用到变量n直接使用第一次读取到的值,这样可以避免每次用到n变量都去n的地址读取值,但是在当异步发生的时候,如果还是用优化操作,那么如果中间信号捕捉函数修改了n值,进程所看见的n值还是第一次读取的值没有跟随改变,就造成了异步错误,所以使用volatile修饰变量n,告诉编译器,不要对变量n进行内存读写优化,进程每次使用到n都去其内存地址读取,这样有效避免了异步错误。

反汇编命令:objdump

jiaojian@KyLin:~/Linux/APUE/signal$ gcc fanhui.c -g
jiaojian@KyLin:~/Linux/APUE/signal$ objdump a.out -dSsx > file

SIGCHLD信号
SIGCHLD信号产生条件
  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时

父进程回收子进程:

pid_t waitpid(pid_t pid, int *status, int options)
[参数]
options:
WNOHANG
没有子进程结束,立即返回,即不阻塞
WUNTRACED
如果子进程由于被停止产生的SIGCHLD, waitpid则立即返回
WCONTINUED
如果子进程由于被SIGCONT唤醒而产生的SIGCHLD, waitpid则立即返回

获取status(通过以下宏函数来解析status):
WIFEXITED(status)
子进程正常exit终止,返回真
	WEXITSTATUS(status)返回子进程正常退出值
    
WIFSIGNALED(status)
子进程被信号终止,返回真
	WTERMSIG(status)返回终止子进程的信号值
    
WIFSTOPPED(status)
子进程被停止(暂停),返回真
	WSTOPSIG(status)返回停止子进程的信号值

WIFCONTINUED(status)
子进程由停止态转为就绪态,返回真

[返回值]
回收成功返回子进程pid,设置非阻塞时,若无子进程结束,返回0
回收失败返回-1

如下代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(char *str)
{
	perror(str);
	exit(1);
}

void do_sig_child(int signo)
{
	int status;
	pid_t pid;
	while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
		if (WIFEXITED(status))
			printf("child %d exit %d\n", pid, WEXITSTATUS(status));
		else if (WIFSIGNALED(status))
			printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
	} 
}

int main(void)
{
	pid_t pid;
	int i;
	//阻塞SIGCHLD
	for (i = 0; i < 10; i++) {
		if ((pid = fork()) == 0)
			break;
		else if (pid < 0)
			sys_err("fork");
	}
	if (pid == 0) {
		int n = 18;
		while (n--) {
			printf("child ID %d\n", getpid());
			sleep(1);
		}
		return i;
	}
	else if (pid > 0) {
		//先设置捕捉
		//再解除对SIGCHLD的阻塞
		struct sigaction act;
		
		act.sa_handler = do_sig_child;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGCHLD, &act, NULL);
		while (1) {
			printf("Parent ID %d\n", getpid());
			sleep(1);
		} 
	}
	return 0;
}
向信号捕捉函数传参
sigqueue
	int sigqueue(pid_t pid, int sig, const union sigval value)
	union sigval {
		int sival_int;
		void *sival_ptr;
};

sigqueue()函数是给指定进程传信号,并且给该信号捕捉函数传参,传的参数为联合体sigval,所以使用该函数给捕捉函数传参时,捕捉函数使用sigaction结构体内的第二个函数原型,即sa_sigaction = act_i,sa_flags = SA_SIGINFO,还有注意一下两点:

  • 进程自己收发信号,在同一地址空间

  • 不同进程间收发信号,不在同一地址空间,不适合传地址

即进程A用sigqueue()函数给另外一个进程B发送信号和捕捉函数传参,传去了一个指针,如果A进程B进程是两个无关的进程,那么该指针无意义,因为AB之间内存空间不共享;如果AB有血缘关系,那么地址空间有意义。这里需要注意。

sigaction中第二个函数原型
void (*sa_sigaction)(int, siginfo_t *, void *)
siginfo_t {
	int si_int; /* POSIX.1b signal */
	void *si_ptr; /* POSIX.1b signal */
	sigval_t si_value; /* Signal value */
	...
}

	sa_flags = SA_SIGINFO
信号中断系统调用

read阻塞读数据时,信号中断系统调用,则read函数返回:

第一种可能:返回部分读到的数据

第二种可能:read调用失败,errno设成EINTER

根据不同系统返回不一样的情况。

你可能感兴趣的:(Linux学习,linux)