linux高级编程笔记(五)——信号

目录

一、信号的概念

1 与信号相关的事件和状态

2 信号编号

3 Linux常规信号一览表

二、发送信号

1 kill函数和kill命令

2 alarm函数

3 setitimer/getitimer函数

三、信号集操作函数

1 信号集设定

2 sigprocmask函数

3 sigpending函数

四、信号捕获

1 signal函数

2 sigaction函数

3 信号捕获过程

五、信号引起的时序竞态

1 pause函数

2 时序竞态问题

3 异步I/O引发的时序竞态

4 可重入和不可重入

六、SIGCHLD信号

1 产生条件

2 借助SIGCHLD信号回收子进程

七、信号传参

1 发送信号传参

2 捕获信号传参 

八、中断系统调用


一、信号的概念

A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似—―异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

信号的特点:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。·

每个进程收到的所有信号,都是由内核负责发送的,内核处理。

1 与信号相关的事件和状态

(1) 产生信号

  • 按键产生,如:Ctrl+c(SIGINT)、Ctrl+z(SIGTSTP)、Ctrl+\(SIGQUIT)
  • 系统调用产生,如:kill,raise,abort
  • 软条件产生,如:定时器alarm
  • 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
  • 命令产生,如:kill命令

(2)抵达:递送并且到达进程。

(3)未决:产生和抵达之间的状态。主要由于阻塞(或屏蔽)导致该状态。

(4)信号处理方式

  • 执行默认动作
  • 忽略(丢弃)
  • 捕捉(调用用户处理函数)

Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后),此时信号处于未决状态。

未决信号集:

  • 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为o。这一时刻往往非常短暂。

  • 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

2 信号编号

不存在编号为0的信号。其中 1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关,名字上区别不大。而前32个名字各不相同。

信号4要素:

1.编号

2.名称

3.事件

4.默认处理动作·

在不同的操作系统下,信号的值可能不相同,但是信号的宏定义对应的内容是相同的。

Term表示终止当前进程.
Core表示终止当前进程,生成core文件Core Dump(Core Dump 用于gdb调试).
Ign表示忽略该信号.
Stop表示停止当前进程.
Cont表示继续执行先前停止的进程.
Linux常规信号一览:

这里特别强调9)SIGkill和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。设置不能将其设置为阻塞。

另外要清楚,只有每一个信号所对应的事件发生了,该信号才会被递送(但不一定抵达),不应该乱发信号!

3 Linux常规信号一览表

1)SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程

2)SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动
作为终止里程。

3)SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。

4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件

5)SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止里程并产生core文件。

6 ) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。

7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。

9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

10)SIGUSE1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

11)SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。

12)SIGUSR2:这是另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。

14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。

15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。

16)SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号(唯一一个默认为忽略动作的信号)。

17)SIGSKFLT:linux早起版本出现的信号,现在仍然保留向后先下兼容。默认动作为终止进程。

18)SIGCONT:如果进程已经停止,则使其继续运行。默认动作为继续/忽略

19)SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

20)SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

21)SIGTSTP:停止进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。

22)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

23)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。

24)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。

25)SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

26)SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。

27)SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。

28)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

29)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。

30)SIGPWR:关机。默认动作为终止进程。

31)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。

32)SIGRTMIN~ 64)SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信
号的默认动作都为终止进程。

二、发送信号

1 kill函数和kill命令

#include <signal.h>
int kill(pid_t pid, int sig);

参数:

pid:

 > 0,sig发送给ID为pid的进程
 == 0,sig发送给与发送进程同组的所有进程
< 0,sig发送给组ID为|-pid|的进程,并且发送进程具有向其发送信号的权限
== -1,sig发送给发送进程有权限向他们发送信号的系统上的所有进程

sig:

待发送的信号,sig为0时,用于检测,特定为pid进程是否存在,如不存在,返回-1。

通过ps ajx可以看到进程id(PID)和进程的组(PGID)等

 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    2 19840     0     0 ?           -1 S        0   0:00 [kworker/u2:2]
    2 20802     0     0 ?           -1 S        0   0:00 [kworker/u2:0]
 1105 21228 21228 21228 ?           -1 Ss       0   0:00 sshd: [accepted]
 3728 21230  3720  3720 ?           -1 S        0   0:00 /bin/sh -c ntpdate -b -d ntpupdate.tencentyun.com 2>&1 | grep 'step time server' | sed -r '
21230 21231  3720  3720 ?           -1 S<       0   0:00 ntpdate -b -d ntpupdate.tencentyun.com
21230 21232  3720  3720 ?           -1 S        0   0:00 grep step time server
21230 21233  3720  3720 ?           -1 S        0   0:00 sed -r s/.*offset(.*)sec.*/\1/
 

代码示例:

#include <stdio.h>
#include <stdlib.h> //perror
#include <unistd.h>//fork/getpid/getppid/sleep
#include <signal.h>//kill

int main(int argc, char *argv[])
{
    pid_t pid = fork();

    if (pid > 0) {
        printf( "parent,pid = %d\n",getpid());
        while(1);
    }else if (pid == 0) {
        printf( "child pid = %d,ppid = %d\n",getpid(),getppid());
        sleep(2);
        kill(getppid(),SIGSEGV);
    }

    return 0;
}

2 alarm函数

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

描述:

设置定时器(闹钟)。在指定seconds 后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一一个定时器。

返回值:

返回0或剩余的秒数,无失败。常用:取消定时器 alarm(0),返回旧闹钟余下秒数。


例: alarm(5)→ 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)
定时,与进程状态无关(自然定时法)就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。·

代码示例:

///
// 统计一秒能进行多少次i++
///
#include <unistd.h>//alarm
#include <stdio.h>

int main()
{
	int i=0;
	alarm(1);

	while(1) {
		i++;
		printf("%d\n",i);
	}
}

实际程序执行时间 = 系统时间+用户时间+等待时间

time ./alarm

3 setitimer/getitimer函数

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value)

 参数:

which 指定定时方式,有如下三种。

(1)自然定时:ITIMER_REAL -> 14)SIGLARM 计算自然时间

(2)虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26)SIGVTALARM 只计算进程占用CPU的时间

(3)运行时计时(用户+内核):ITIMER_PROF -> 27)SIGPROF 计算占用CPU及系统调用的时间

new_value 传入参数,定时时间

old_ value 传出参数,上次定时剩余时间

struct itimerval {
           struct timeval it_interval; /* 用来设定两次定时任务之间的间隔时间 */
           struct timeval it_value;    /* 定时的时长 */
 };

struct timeval {
             time_t      tv_sec;         /* seconds */
             suseconds_t tv_usec;        /* microseconds */
 };

代码示例: 

#include <stdio.h>
#include <unistd.h>//setitimer
#include <signal.h>//signal
#include <sys/time.h>//itimerval

void func(int signo)
{
        printf("hello %d\n",signo);
}


int main()
{
        struct itimerval it,oldit;

        signal(SIGALRM,func);//注册SIGALARM信号的捕捉函数

        it.it_value.tv_sec = 2;
        it.it_value.tv_usec = 0;

        it.it_interval.tv_sec = 5;
        it.it_interval.tv_usec = 0;


        if(setitimer(ITIMER_REAL,&it,&oldit)==-1)
        {
                perror("setitimer error");
                return -1;
        }

        while(1);
        return 0;


}

三、信号集操作函数

1 信号集设定

sigset_t set;//信号集类型

#include <signal.h>
int sigemptyset(sigset_t *set);//清空信号集set,所有二进制位全设置0
int sigfillset(sigset_t *set);//填充信号集set,所有二进制位设置为1
int sigaddset(sigset_t *set, int signum);//向集和set中添加一个信号
int sigdelset(sigset_t *set, int signum); //向集和set中删除一个信号
int sigismember(const sigset_t *set, int signum);//判断信号signum是否在信号集set中,存在返回1,不在返回 0,出错返回-1

2 sigprocmask函数

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

描述:

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

  1. SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
  2. SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
  3. SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set

3 sigpending函数

#include <signal.h>
int sigpending(sigset_t *set);

读取当前进程的未决信号集

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

//打印信号集
void print_set(const sigset_t set) 
{
	int i;
	for (i=1;i<32;i++)
	{
		if(sigismember(&set,i))
		{
			putchar('1');
		}
		else
		{
			putchar('0');
		}
	}

	printf("\n");
}

int main()
{
	sigset_t set,oldset,pdset;
	sigemptyset(&set);
	sigaddset(&set,SIGINT);

	int ret = sigprocmask(SIG_BLOCK,&set,&oldset);//设置信号屏蔽字

	if(ret==-1)
	{
		perror("sigprocmask err");
		exit(1);
	}

		
	while(1)
	{
		ret = sigpending(&pdset);//获取未决信号集
	
		if(ret == -1)
		{
			perror("sigpending err");
			exit(1);
		}
		
		print_set(pdset);	//打印信号屏蔽字
	}
	return 0;
}

运行程序之后,可以看到当键入Ctrl+C(SIGINT)之后,由于2号信号(SIGINT)被屏蔽,将一直保留在未决信号集中而不被处理。

需要注意的是:9)号信号SIGKILL和19)号信号不能被屏蔽,即使设置了信号屏蔽字也无法屏蔽。只能执行默认动作。

四、信号捕获

1 signal函数

#include <signal.h>
typedef void (*sighandler_t)(int); //定义一个函数指针,一个int类型参数,void类型返回值
sighandler_t signal(int signum, sighandler_t handler);

描述:

注册一个信号捕捉函数

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void sig_cat(int signo)
{
	printf("catch you:%d\n",signo);
}

int main()
{
	signal(SIGINT,sig_cat);//注册信号捕获函数	

	while(1);
	
	return 0;	
}

运行程序之后,按下Ctrl+C,将会被捕获

2 sigaction函数

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);//由于有void*参数,可以携带复杂的数据
               sigset_t   sa_mask;//工作于信号捕捉函数执行期间屏蔽sa_mask对应的信号
               int        sa_flags;//设置为0时,默认屏蔽当前信号捕获函数对应的信号。例如,注册了SIGINT的捕获函数fun,当正在执行fun时,SIGINT再次到来时会被屏蔽
               void     (*sa_restorer)(void);
 };

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void sig_cat(int signo)
{
        printf("catch you:%d\n",signo);
}

int main()
{
        struct sigaction act,oldact;

        act.sa_handler = sig_cat;//设置信号捕获函数
        sigemptyset(&act.sa_mask);//设置信号屏蔽字,只在sig_cat函数执行中有效
        act.sa_flags = 0;         //设置默认属性

        int ret = sigaction(SIGINT,&act,&oldact);

        if(ret==-1)
        {
                perror("sigaction err");
                exit(1);
        }

        while(1);

        return 0;
}

信号捕捉特性:

1.进程正常运行时,默认 PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask 来指定。调用完信号处理函数,再恢复为☆。
2. xXx信号捕捉函数执行期间,xXx信号自动被屏蔽。
3.阻塞的常规信号不支持排队,产生多次只记录一次。(在处理信号捕获函数期间,多次发送信号,当函数返回后,该信号只会被处理一次。后32个实时信号支持排队)。

3 信号捕获过程

五、信号引起的时序竞态

1 pause函数

#include <unistd.h>
int pause(void);

操作系统内唯一一个主动造成进程挂起的系统调用。调用该系统调用的进程将处于阻塞状态(主动放弃cpu)直到有信号递达将其唤醒。

返回值:

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

代码示例:

// 实现sleep函数工程
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sig_alrm(int signo)
{

}
unsigned 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);//定时nsecs秒
        pause();

        unslept = alarm(0);//取消闹钟,防止pause在alarm超时之前被其他信号唤醒,导致sig_alrm错误调用
        sigaction(SIGALRM,&oldact,NULL);//将调用mysleep函数的程序中的信号行为恢复,避免调用之后修改调用者的信号逻辑

        return unslept;
}

int main()
{
        while(1)
        {
                printf("定时两秒\n");
                mysleep(2);
        }
}

【思考】

当alarm执行之后,如果此时因某种情况失去CPU,时间超过设定定时的时间,那么在得到CPU之前,操作系统会根据执行alarm的行为发送SIGALRM信号。再次得到CPU时,该程序已经不会受到SIGALRM信号了,那么进程将会阻塞在pause函数,这就是时序竞态。
那么如何解决这个问题呢?

2 时序竞态问题

可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去 cpu资源。除非将这两步骤合并成一个“原子操作"”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。

#include <signal.h>
int sigsuspend(const sigset_t *mask);

描述:

暂时用mask给出的掩码替换调用进程的信号掩码,然后挂起进程,直到传递信号,其操作是调用信号处理程序或终止进程

改进的sleep代码如下:

// 实现sleep函数工程
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sig_alrm(int signo)
{

}
unsigned 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);

        //屏蔽SIGALRM信号
        sigemptyset(&newmask);
        sigaddset(&newmask, SIGALRM);
        sigprocmask(SIG_BLOCK, &newmask, &oldmask);//当向信号屏蔽字中添加SIGALRM,此时若出现时序竞态的情况,SIGALRM会被屏蔽而进入未决信号集

        alarm(nsecs);
        
        
        suspmask = oldmask;
        sigdelset(&suspmask, SIGALRM);//为了不解除其他信号而影响程序逻辑,这里将系统屏蔽字拿出来之后,删除SIGALRM信号,即可解除对SIGALRM的屏蔽

        //解除屏蔽和等待作为一个原子操作
        sigsuspend(&suspmask);

        unslept = alarm(0);//取消闹钟,防止pause在alarm超时之前被其他信号唤醒,导致sig_alrm错误调用
        sigaction(SIGALRM,&oldact,NULL);//将调用mysleep函数的程序中的信号行为恢复,避免调用之后修改调用者的信号逻辑

        sigprocmask(SIG_SETMASK,&oldmask,NULL);//恢复原先的信号屏蔽字

        return unslept;
}

int main()
{
        while(1)
        {
                printf("定时两秒\n");
                mysleep(2);
        }
}

3 异步I/O引发的时序竞态

使用SIGUSE1SIGUSE2实现父子进程交替数数。

代码示例:

#include <signal.h>//kill
#include <unistd.h> //fork/getpid
#include <stdlib.h>//perror
#include <stdio.h>

int n = 0;
int flag = 0;//flag用来标志当前进程的状态,1表示数完数字,0表示发完信号。初始为0表示等待数数

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

void do_sig_child(int num)
{
	printf("I'm child %d \t %d\n",getpid(),n);
	n += 2;
	flag = 1;
}


void do_sig_parent(int num)
{
	printf("I'm parent %d \t %d\n",getpid(),n);
	n += 2;
	flag = 1;
}

int main()
{

	pid_t pid = fork();
	struct sigaction act;
	
	if(pid<0) sys_err("fork error");
	else if(pid == 0)//子进程
	{
		n = 1; //子进程数基数
		
		act.sa_handler = do_sig_child;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGUSR1,&act,NULL);

		while(1)
		{
			if(flag)
			{
				kill(getppid(),SIGUSR2);
				flag = 0;
			}
		}
	}
	else if (pid > 0)//父进程,从父进程开始数数
	{
		sleep(1);//让子进程有充分之间注册信号
		
		act.sa_handler = do_sig_parent;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(SIGUSR2,&act,NULL);

		do_sig_parent(0);//父进程开始数数,父进程数偶数

		while(1)
		{
			if(flag)
			{
				kill(pid,SIGUSR1);
				flag = 0;
			}
		}
		
	}
	
	return 0;
}

执行程序后发现,程序会卡在数到5027的位置(这个是随机的位置,可能会卡在其他位置)。

示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。问题出现的位置,在父子进程 kill函数之后需要紧接着调用flag,将其置0,标记信号已经发送。回顾之前的时序竞态,在父进程执行kill发送SIGUSE1设置flag标志位这期间很有可能被kernel调度,失去执行权利,而子进程获取了执行时间,通过发送信号回调捕捉函数,子进程的信号不会再次发送到父进程。当父进程获得CPU后,flag被设置为0。至此以后,父进程不会再收到子进程的信号来修改flag位,从而导致阻塞。

如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作外部作用域的变量的时候,通过加锁、解锁来解决该问题。也就是说,在发送信号和修改flag位的时候加锁。现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步lO可能造成的问题。

4 可重入和不可重入

上图中,inser函数为不可重入,由于内部使用了全局变量,当第一次插入insert函数时,在执行head==p和p->next=head之间因为一个信号打断被插入了node2,最终结果为第4步的结果,程序的逻辑发生了改变。

注意事项

  1. 欲封装自定义可重入函数,该函数内不能含有全局变量及static变量,不能使用malloc、free
  2. 信号捕捉函数应设计为可重入函数。
  3. 信号处理程序可以调用的可重入函数可参阅man 7 signal。
  4. 没有包含在上述列表中的函数大多是不可重入的,其原因为: 

    a)使用静态数据结构。

    b)调用了malloc或free

    c)是标准l/O函数

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

六、SIGCHLD信号

1 产生条件

  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时。

2 借助SIGCHLD信号回收子进程

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
void catch_child(int signo)
{
        pid_t wpid;

        while((wpid = wait(NULL))!=-1) //防止多个子进程发送信号排队而只执行一次
        {
                printf("catch child id:%d\n",wpid);
        };

        return;
}

int main()
{
        pid_t pid;

        int i;


        for(i=0;i<5;i++)
        {
                if((pid=fork())==0)
                {
                        break;
                }

        }

        if(5==i){

                struct sigaction act;
                act.sa_handler = catch_child;
                sigemptyset(&act.sa_mask);
                act.sa_flags = 0;
                sigaction(SIGCHLD, &act, NULL); //父进程注册SIGCHLD信号捕获函数

                printf("I'm parent, pid=%d\n",getpid());
                while(1);
        }
        else
        {
                printf("I'm child pid=%d\n",getpid());
                //子进程退出时会向父进程发送SIGCHLD信号
        }

        return 0;
}

七、信号传参

1 发送信号传参

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

union sigval {
               int   sival_int;
               void *sival_ptr;//由于进程之间数据不共享,一般用来给自己发送该数据。
};

2 捕获信号传参 

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);//由于有void*参数,可以携带复杂的数据
               sigset_t   sa_mask;//工作于信号捕捉函数执行期间屏蔽sa_mask对应的信号
               int        sa_flags;//设置为0时,默认屏蔽当前信号捕获函数对应的信号。例如,注册了SIGINT的捕获函数fun,当正在执行fun时,SIGINT再次到来时会被屏蔽
               void     (*sa_restorer)(void);
 };

siginfo_t {
               int      si_signo;     /* Signal number */
               int      si_errno;     /* An errno value */
               int      si_code;      /* Signal code */
               int      si_trapno;    /* Trap number that caused
                                         hardware-generated signal
                                         (unused on most architectures) */
               pid_t    si_pid;       /* Sending process ID */
               uid_t    si_uid;       /* Real user ID of sending process */
               int      si_status;    /* Exit value or signal */
               clock_t  si_utime;     /* User time consumed */
               clock_t  si_stime;     /* System time consumed */
               sigval_t si_value;     /* Signal value */
               int      si_int;       /* POSIX.1b signal */
               void    *si_ptr;       /* POSIX.1b signal */
               int      si_overrun;   /* Timer overrun count;
                                         POSIX.1b timers */
               int      si_timerid;   /* Timer ID; POSIX.1b timers */
               void    *si_addr;      /* Memory location which caused fault */
               long     si_band;      /* Band event (was int in
                                         glibc 2.3.2 and earlier) */
               int      si_fd;        /* File descriptor */
               short    si_addr_lsb;  /* Least significant bit of address
                                         (since Linux 2.6.32) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */

}

 当注册信号捕捉函数,希望获取更多信号相关信息,不应使用sa_handler而应该使用sa_sigaction。但此时的sa_flags必须指定为SA_SIGINFO。siginfo_t是一个成员十分丰富的结构体类型,可以携带各种与信号相关的数据。

八、中断系统调用

系统调用可分为两类:慢速系统调用和其他系统调用。·
1.慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait...
2.其他系统调用:getpid、getppid、fork...
结合pause,回顾慢速系统调用:慢速系统调用被中断的相关行为,实际上就是pause的行为。如,read

  • 想中断pause,信号不能被屏蔽。
  • 信号的处理方式必须是捕捉(默认、忽略都不可以)
  • 中断后返回-1,设置errno为EINTR(表“被信号中断”)

可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。SA_RESTART重启。

扩展了解:sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER(捕捉函数执行期间信号不会被屏蔽),除非sa_mask 中包含该信号。
 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值