信号

信号知识结构图

目录

 

信号概念

信号在我们生活中随处可见,如古代战争中摔杯为号,现代战争中的信号弹,体育比赛中的信号枪....,它们共性:1、简单,2、不携带大量信息,3、满足某个特设条件才发送。

信号是信息的载体,Linux/UNIX环境下,古老、经典的通信方式,现在依然是主要 的通信手段。

信号机制

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

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

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

与信号相关的事件和状态

产生信号

  • 1.按键产生,如Ctrl+c(向内核发送SIGINTx信号,终止);
  • 2.系统调用产生,如,kill,raise,abort;
  • 3.软件条件,如:定时器alarm
  • 4.硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误
  • 5.命令产生,如kill命令

递达递达并且到达进程

未决产生和递达之间的状态,主要由于阻塞屏蔽)导致该状态。

信号的处理方式

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

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

阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,在收到该信号,该信号的处理将推后(解除屏蔽后)sigprocmask函数,操作信号屏蔽字来影响未决信号集。

未决信号集(默认为0值)

  1. 信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信息被处理对应位翻转回为0。这一时刻往往非常短暂。
  2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态

信号编码

使用kill -l命令查看当前系统可使用的信号有那些。

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

信号4要素

与变量三要素类似的,每个信号也有其必备4要素,分别是:1、编号 2、名称,3、事件,4、默认处理动作

可通过man 7 signal查看帮助文档获取

Signal     	Value     Action   Comment
────────────────────────────────────────────
SIGHUP     	 1       Term    Hangup detected on controlling terminal or death of controlling process
SIGINT      	 2       Term    Interrupt from keyboard
SIGQUIT     	 3       Core    Quit from keyboard
SIGILL       	 4       Core    Illegal Instruction
SIGFPE      	 8       Core    Floating point exception
SIGKILL      	 9       Term    Kill signal
SIGSEGV    	 11      Core    Invalid memory reference
SIGPIPE  	 13      Term    Broken pipe: write to pipe with no readers
SIGALRM   	 14      Term    Timer signal from alarm(2)
SIGTERM    	 15      Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
SIGCHLD   20,17,18    Ign     Child stopped or terminated
SIGCONT   19,18,25    Cont    Continue if stopped
SIGSTOP   17,19,23    Stop    Stop process
SIGTSTP   18,20,24    Stop    Stop typed at terminal
SIGTTIN   21,21,26    Stop    Terminal input for background process
SIGTTOU   22,22,27   Stop    Terminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.   	

在标准信号中,有一些信号是有三个“value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构,一个‘-’表示在对应架构上尚未定义该信号。

不同的操作系统定义了不同的系统信号,因此有些信号出现在Unix系统内,也出现Linux中,而有的信号出现在FreeBSD或MacOS中却没有出现在Linux下,这里只研究Linux系统中的信号。

默认动作

  • Term:终止进程
  • Ign:忽略信号(默认即时对该种信号忽略操作)
  • core:终止进程,生成Core文件,(查验进程死亡原因,用于gdb调试)
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

注意:9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞

只有每个信号所对应的时间发送了,该信号才会被递达(但不一定递达),不应乱发信号。

产生信号

终端按键

  • Ctrl + c  --->2)SIGINT(终止/中断)        ‘INT’ ---- interrupt
  • Ctrl + z ---> 20)SIGTSTP(暂停/停止)   “T” -----Terminal终端
  • Ctrl +\ ----> 3)SIGQUT(退出)

硬件异常

  • 除0操作  -------> 8)SIGFPE(浮点数例外)      “F” ----float 浮点数。
  • 非法访问内存-------> 11)SIGSEGV(段错误)
  • 总线错误---------->7)SIGBUS

函数/命令

kill 命令产生信号:kill -SIGKILL pid

kill函数:给指定进程发送指定信号(不一定杀死)

int kill(pid_t pid,int sig);//成功返回0;失败-1(ID非法,信号非法,普通用户杀int进程等权级问题,设置errno)

  • sig:   不推荐直接使用数字,应使用宏名,不同操作系统信号编号可能不同,但名称一致
  •  pid>0: 发送信号给指定信号(不一定杀死)
  •  pid=0 发送信号给调用kill函数进程属于同一进程组的所有进程
  •  pid<0: 取|pid|发给对应进程组
  •  pid=-1:发送给进程有权限发送的系统中所有进程。wait = -1,回收所有子进程

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们互相关联,共同完成一个实体任务,每个进程组都有一个进程组长默认进程组ID与进程组长ID相同

权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能像系统用户发送信号的,kill -9 (root用户的pid)是不可以的,同样,普通用户也不能像其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接受者实际或有效用户ID;?

练习:循环创建5个子进程,任一子进程用kill函数终止其父进程。【kill.c】

kill 任一子进程,其余子进程也被kill,子进程之间是用管道通信,和管道通信之间返回值有关。

raise和abort函数

raise函数:给当前进程发送指定信号(自己给自己发) raise(signo) == kill(getpid(),signo);

int raise(int sig);//成功:0,失败非0值

abort函数给自己发送异常终止信号 6)SIGABRT信号,终止并产生core文件;

软件条件

alarm函数

设置定时器(闹铃)。在指定seconds后,内核会给当前进程发送 14)SIGALRM信号进程收到该信号,默认动作终止

每个进程都有且只有唯一一个定时器。

unsigned int alarm(unsigned int seconds);//返回0或剩余的秒数,无失败

常用取消定时器alarm(0),返回旧闹钟余下秒数

例:alarm(5) --->3sec --->alarm(4)---->5sec----->alarm(5)----->alarm(0)

定时,与进程状态无关(自然定时法),就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时

练习:编写程序,测试你使用的计算机1秒钟数多少个数。【alarm.c】

使用time命令查看程序执行的时间,程序运行的瓶颈在于IO,优化程序,首选优化IO

实际执行时间 = 系统时间+用户时间+等待时间(程序消耗等待时间在与文件IO上)

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

int main(void){
        int i;
        unsigned int re;
//设置定时器,在指定seconds之后,内核给当前进程发送14)信号,进程收到信号,默认动作终止
        re = alarm(1);
//取消定时器
//      alarm(0);
        //printf("%d\n",re);
        for(i =0; ;i++)
                printf("%d\n",i);
        return 0;
}

设置定时器,在指定 1seconds之后,内核给当前计数进程发送14SIGALRM信号,进程收到信号,默认动作终止;注意区分setitmer()、sleep(),man alarm:

  • alarm() and setitimer(2) share the same timer; calls to one will interfere with use of the other.
  •  sleep(3) may be implemented using SIGALRM; mixing calls to alarm() and sleep(3) is a bad idea.
  • Scheduling delays can, as ever, cause the execution of the process to be delayed by an arbitrary amount of time.

setitimer函数

设置定时器(闹钟),可替代alarm函数,精度微秒us,可以实现周期定时。

int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
//成功:0,失败:-1,设置errno

参数:which 指定定方式

  • 自然定时: ITIMER_REAL -->14)SIGLARM
  • 虚拟空间计时(用户空间):ITIMER_VIRTUAL -->16)SIGVTALRM  只计算进程占cpu时间
  • 运行时计时(用户+内核):ITIMER_PROF---->27)SIGPROF   计算占用cpu及执行系统调用的时间

练习:使用setitimer函数实现alarm函数,重复计算机1秒数数程序。 【setitimer.c】

拓展练习,结合man page编写程序,测试it_interval,t_value 这两个参数的作用;【setitimer1.c】

提示:it_interval:用来设定两次定时任务之间间隔的时间,it_value:定时的时长,两个参数都设置为0,即清0操作


#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>

/*
struct itimerval{
        struct timeval{
                it_value.tv_sec;
                it_value.tv_usec;
        }it_interval;

        struct timeval{
                it_value.tv_sec;
                it_value.tv_usec;
        }it_value;
} it,oldit;
*/
unsigned int my_alarm(unsigned int sec){
        struct itimerval it,oldit;
        int ret;

        it.it_value.tv_sec = sec;
        it.it_value.tv_usec = 0;
        it.it_interval.tv_sec =0;
        it.it_interval.tv_usec = 0;

        ret = setitimer(ITIMER_REAL,&it,&oldit);
        if(ret == -1){
                perror("setitimer");
                exit(1);
        }
        return oldit.it_value.tv_sec;

}

int main(void){
        int i;
        my_alarm(1);//alarm(sec);

        for(i = 0; ;i++)
                printf("%d\n",i);

        return 0;
}

      The  system  provides  each process with three interval timers, each decrementing in a distinct time domain.  When any timer expires, a signal is sent to the process, and the timer (potentially) restarts.

       ITIMER_REAL    decrements in real time, and delivers SIGALRM upon expiration.

       ITIMER_VIRTUAL decrements only when the process is executing, and delivers SIGVTALRM upon expiration.

       ITIMER_PROF    decrements both when the process executes and when the system is executing on behalf of the process.  Coupled with ITIMER_VIRTUAL, this  timer  is  usually used to profile the time spent by the application in user and kernel space.  SIGPROF is delivered upon expiration.

       Timer values are defined by the following structures:

struct itimerval {
 struct timeval it_interval; /* next value */
struct timeval it_value;    /* current value */
};

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

信号集操作函数

内核通过读取未决信号集来判断信号是否应被处理信号屏蔽字mask影响未决信号集,而我们可以在应用程序中自定义set来改变mask已达到屏蔽指定信号目的

信号集设定

sigset_t set;//typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set);    //将某个信号集清0  成功0,失败-1
int sigfillset(sigset_t *set);    //将某个信号集置1   成功 0,失败-1
int sigaddset(sigset_t *set,int signum);//将谋个信号加入信号集  成功0,失败-1、
int sigdelset(sigset_t *set,int signum);//将某个信号清出信号集  成功0 ,失败-1
int sigismember(const sigset_t *set,int signum);//判断某个信号是否在信号集中, 返回值:在集合1,不在0,出错-1

sigset_t类型的本质是位图,但不应该直接使用位操作,应该使用上述函数保证夸系统操作有效,可对比认知select函数

sigprocmask函数

用来屏蔽信号、解除屏蔽,本质为读取或修改进程的信号屏蔽字(PCB中)。严格注意屏蔽信号:只是将信号处理延后执行(延迟解除屏蔽),而忽略表示将信号丢处理

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);//成功:0,失败:-1,设置errno

参数:

set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽那个信号

oldset:传出参数,保存旧的信号屏蔽集

how参数取值:假设当前的信号屏蔽字为mask

  • SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号,相当于 mask = mask | set
  • SIG_UNBLOCK:当how设置为此值,set表示需要解除屏蔽的信号,相当于 mask = mask&~set
  • SIG_SETMASK:当前how设置为此值,set表示用于替代原始屏蔽及新屏蔽集,相当于mask = set;若调用sigpromask解除了对当前若干个信号的阻塞,则在sigprocmask返回前至少将其中一个信号递达

sigpending函数

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

int sigpending(sigset_t *set);  //set传出参数, 返回值:成功:0,失败:-2,设置errno

【练习】编程程序,把所有常规信号的未决状态打印至屏幕。【sigpending.c】


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

void printset(sigset_t *ped){
        int i;
        for(i =1;i<32;i++){
                if(sigismember(ped,i) == 1){//未决信号ped是否在信号集内,是返回1,否返回0
                        putchar('1');
                }else{
                        putchar('0');
                }
        }
        printf("\n");
}

int main(void){
        sigset_t set,ped;
//#if 0
        sigemptyset(&set);//初始化信号设置
        sigaddset(&set,SIGINT);//向2号信号集里添加信号
        sigaddset(&set,SIGQUIT);//3好信号
        //sigaddset(&set,SIGKILL);
        //sigaddset(&set,SIGSEGV);
//#endif
        //sigfillset(&set);//initializes set to full, including all signals
        sigprocmask(SIG_BLOCK,&set,NULL);//屏蔽信号(解除屏蔽),这里用于屏蔽信号,NULL->传出参数== &oldit

        while(1){
                sigpending(&ped);//读取当前进程中的未决信号
                printset(&ped);
                sleep(1);
        }

        return 0;
}
     

 

sigset_t定义set信号sigemptyset函数先讲信号清0sigaddset函数向信号集内添加信号sigprocmask函数设置屏蔽字,进而影响未决信号集,使用键盘产生信号Ctrl c(SIGINT)、Ctrl \(SIGQUIT)信号(当键盘按下特定组成键,驱动程序会检测到),sigpending函数读取当前进程中的未决信号

此处为按下键盘Ctrl c 产生2)SIGINT信号,该信号被设为屏蔽,收到该信号,该信号的处理将推后

打印为0,表示该信号没有在信号集内,打印为1表示在信号集内;

sigprocmask(SIG_BLOCK,&set,NULL));先用于屏蔽信号

sigidmember(ped,i);发送信号1-32,根据返回值,判断该信号是否处于未决状态

int putchar(int ch);//写字符到stdout,在内部,字符写入前被转换到unsigned char,等价于putc(ch,stdout);
//成功:返回写入的字符,失败:返回EOF

有点不理解:与练习中的未决信号有什么关系?

使用3号信号SIGQUIT来初始化信号set,ped,键盘Ctrl \可以触发3号信号,接着使用sigprocmask来屏蔽信号,这里屏蔽什么信号?当键盘按下Ctrl \ 产生3号信号(该信号被设置为屏蔽)无法递达处于未决状态(相当于模拟屏蔽信号导致阻塞,使得信号处于未决状态),sigpending函数读取当前进程中的未决信号ped,使用sigidmember函数判断发送的信号是否在未决信号集ped内,这里信号集ped表示未决信号;

内核读取未决信号集判断该信号是否应该被处理(信号产生后由于阻塞等原因不能抵达,处于未决状态,在屏蔽解除前,信号一直处于未决状态)

信号捕捉

signal函数

用于注册信号捕捉函数

typedef void(*sighander_t)(int);
sighander_t signal(int signum,sighander_t hander);

该函数有ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为,因此尽量避免使用它, 取而代之使用sigaction函数

void (*signal(int signum, void(*sighander_t)(int)))(int);

函数代表什么意思?空指针类型函数? 注意多在复杂结构中使用typedef.

sigaction函数

用于修改信号处理动作(通常在Linux中用其注册一个捕捉函数);

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
//成功:返回0,失败:返回-1,设置errno
  • act:      传入参数,新的处理方式;
  • 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_restorer:该元素是过时的,POSIX.1标准将不指定该元素。(弃用)
  • sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)

重点掌握

  • sa_hander:指定信号捕捉后的处理函数名(即注册函数)。可赋值为SIG_IGN表忽略SIG_DFL表执行默认动作
  • sa_mask:调用信号处理函数时,所要屏蔽的信号集(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置
  • sa_flags:通常设置为0,表使用默认属性

捕捉信号特性

进程正常运行时,默认PCB中有一个信号屏蔽字,假定为*,这决定了进程自动屏蔽那些信号。当注册了某个信号捕捉函数捕捉到该信号以后要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由*来指定调用完信号处理函数,在恢复为*

xxx信号捕捉函数执行期间,xxx信号自动被屏蔽

阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队

【练习1】为某个信号设置捕捉函数 【sigaction1.c】

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

void sig_int(int signo){
        printf("catch signal SIGINT\n");
        sleep(10);
        printf("------slept 10 s\n");
}

int main(){
        struct sigaction act;
        act.sa_handler = sig_int;
        act.sa_flags = 0;//递进来的标志位,默认0:信号捕捉函数执行期间,自动屏蔽所设置的屏蔽信号
        sigemptyset(&act.sa_mask);//将信号清0
        sigaddset(&act.sa_mask,SIGQUIT);//将SIGQUIT信号加入信号集

        sigaction(SIGINT,&act,NULL);//注册一个信号捕捉函数,修改信号处理动作,这里会执行sig_int函数,SIGINT为一个有效信号

        printf("--------main slept 10\n");
        sleep(10);

        while(1);
        return 0;
}

sigaction函数用于注册一个信号捕捉函数sig_int修改当前进程信号处理动作;其中传入参数act,struct sigaction类型的结构体,主要对特定信号的处理,信号所传递的信息,信号处理函数执行过程中应屏蔽掉哪些函数等;

sigemptyset函数用来将信号清0sigaddset函数将指定屏蔽信号添加到信号集内

【练习2】验证在信号处理函数执行期间,该信号多次递送,那么只在处理函数之前结束后,处理一次。【sigaction2.c】

键盘按下Ctrl c产生信号SIGINT被sigaction函数捕捉到执行系统调用函数sig_int函数,打印catch sigal SIGINT,在执行信号处理函数sig_int函数期间多次产生SIGINT信号只能等待sig_int函数处理结束之后,再次调用,但只处理一次

【练习3】验证sa_mask捕捉函数执行期间的屏蔽作用。【sigaction3.c】

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

void sig_int(int signo)
{
	printf("catch signal SIGINT\n");
	sleep(10);			//睡眠10s
	printf("end of handler\n");
}

int main(void)
{
//定义一个捕捉信号
	struct sigaction act;		

//初始化信号处理函数sig_int
	act.sa_handler = sig_int;
//对信号集做清0,sa_mask为设置信号屏蔽字
	sigemptyset(&act.sa_mask);	
   //将信号SIGQUIT添加到信号集内,	
	sigaddset(&act.sa_mask, SIGQUIT);	

	/*通常设置为0,表示默认值*/
	act.sa_flags = 0;
//注册一个信号的捕捉函数,
	sigaction(SIGINT, &act, NULL);		//发送信号SIGINT时,系统调用信号捕捉函数sig_int()

	while(1);

	return 0;
}

sigaddset(&act.sa_mask,SIGQUIT);向信号集添加了SIGQUIT信号,但按键Ctrl \时直接退出了进程没有起到屏蔽的作用,怎么回事?还有sa_mask是调用信号处理函数时,所有屏蔽的信号集合(信号屏蔽字)。值得注意的是它仅在处理函数被调用期间屏蔽生效,是临时性设置,处理函数被执行完sa_mask失效;

内核实现信号捕捉过程

竞态条件

pause函数

调用该函数可以造成进程主动挂起等待信号唤醒;调用该系统调用的进程将处于阻塞状态主动放弃cpu直到有信号递达将其唤醒

int pause(void);//返回值:-1,并设置errno为EINTR

返回值

  • 信号的默认处理动作终止进程,则进程终止,pause函数有机会返回
  • 若信号的默认处理动作是忽略进程继续处于挂起状态pause函数不返回
  • 若信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】,errno设置为EINTR,表示“被信号中断”;
  • pause收到信号不能被屏蔽如果被屏蔽,那么pause就不能被唤醒

【练习】使用pause和alarm来实现sleep函数。【mysleep.c】

注意:unslept = alarm(0)的用法;例如:睡觉,alarm(10)闹铃,正常:10后闹铃将我唤醒,这是额外设置alarm(0)取消闹铃,不会出错;异常:5分钟,被其他事物吵醒,alarm(0)取消闹铃防止打扰;

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

void sig_alrm(int signo){
        /*nothing to do*/
}

unsigned int mysleep(unsigned int nsecs){
        //sigaction:注册SIGALRM信号处理函数
        struct sigaction newact,oldact;
        unsigned int unslept;
        //设置回调函数,内核会自动调用sig_alrm
        newact.sa_handler = sig_alrm;
        //将某个信号清0,成功返回0,失败返回-1
        sigemptyset(&newact.sa_mask);

        newact.sa_flags = 0;
        sigaction(SIGALRM,&newact,&oldact);
        //设置定时闹钟,精确到微秒
        alarm(nsecs);//计时
        //pause函数可造成进程主动挂起,等待信号唤醒
        pause();
        //用户态进程阻塞,必须要信号唤醒,否则会一直阻塞,怎么去发信号,解除阻塞
        unslept = alarm(0);//设置 取消闹铃
        //内核态,发送信号,唤醒pause();用户态继续执行pause();阻塞,涉及内核到用户态的转变
        sigaction(SIGALRM,&oldact,NULL);

        return unslept;
}

int main(){
        while(1){
                mysleep(2);
                printf("two seconds passed\n");
        }


        return 0;
}
       

mysleep.c代码中,当alarm()函数执行完毕,同时失去cpu(其他优先级高程序执行),等到其他程序执行完毕,当前进程获取CPU,先处理alarm调用结束,pause函数被SIGALARM信号唤醒返回-1(成功);

alarm调用结束内核向当前进程发送SIGALRM信号,信号被捕捉函数捕捉,调用完处理函数之后(即信号已经被捕捉),pause函数无法收到即无法被唤醒,当前进程会被主动挂起

gdb调试 单步执行到pause之后程序不法继续执行,是否与pause函数使得当前进程被挂起等待有关?

(gdb) break 34
Breakpoint 1 at 0x4006f8: file mysleep.c, line 34.
(gdb) break 14
Breakpoint 2 at 0x400677: file mysleep.c, line 14.
(gdb) run
Starting program: /root/test/mysleep 

Breakpoint 1, main () at mysleep.c:34
34			mysleep(2);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-292.el7.x86_64
(gdb) next

Breakpoint 2, mysleep (nsecs=2) at mysleep.c:14
14		newact.sa_handler = sig_alrm;
(gdb) next
16		sigemptyset(&newact.sa_mask);
(gdb) next
18		newact.sa_flags = 0;
(gdb) next
19		sigaction(SIGALRM,&newact,&oldact);
(gdb) next
21		alarm(nsecs);//计时
(gdb) next
23		pause();
(gdb) next
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ad2790 in __pause_nocancel () from /lib64/libc.so.6
(gdb) quit
A debugging session is active.

	Inferior 1 [process 396601] will be killed.

Quit anyway? (y or n) y

时序竞态

先来看个例子,睡觉前设定10分钟闹铃;正常情况下定时睡觉,10分钟被闹铃唤醒;异常:闹铃设定好,有事外出20分钟,回来后继续睡觉,但外出期间闹铃已经响过,不会在将我叫醒;

这是涉及时序问题,借助pause和alarm实现mysleep函数,设想如下时序:

  1. SIGALRM信号处理函数
  2. 调用alarm(1)函数设定闹铃1秒
  3. 函数调用结束,开始倒计时1秒,当前进程失去CPU内核调度优先级高的进程(有多个)取代当前进程,当前进程无法获得CPU,进入就绪态等待CPU
  4. 1秒后,闹铃超时内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高级优先级尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
  5. 优先级高的进程执行完,当前进程获得CPU资源,内核调度会当前进程执行SIGALRM信号递达信号设置捕捉,执行处理函数sig_alarm;
  6. 信号处理函数执行结束,返回当前进程主控流程pause()被调用挂起等待,欲等待alarm函数发送SIGALRM信号将自己唤醒
  7. SIGALRM信号已经处理完毕,pause不会等到

解决时序问题

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

int sigsuspend(const sigset_t *mask);//挂起等待信号

sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定

【练习】改进版mysleep。【sigsuspend.c】

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

void sig_alarm(){
        /*nothing to do*/

}

unsigned int mysleep(unsigned int nsecs){
        struct sigaction newact,oldact;
        sigset_t newmask,oldmask,suspmask;
        unsigned int unslept;
        //1为SIGALRM设置捕捉函数,一个空函数
        newact.sa_handler = sig_alarm;
        sigemptyset(&newact.sa_mask);
        newact.sa_flags = 0;
        sigaction(SIGALRM,&newact,&oldact);

        //2设置阻塞信号集,阻塞SIGALRM信号
        sigemptyset(&newmask);
        sigaddset(&newmask,SIGALRM);
        sigprocmask(SIG_BLOCK,&newmask,&oldmask);//信号屏蔽字

        //3定时n秒,到时候可以产生SIGALRM信号
        alarm(nsecs);
        /*4 构造一个调用sigsuspend临时有效的阻塞信号集
        在临时阻塞信号集里解除SIGALRM的阻塞*/
        suspmask = oldmask;
        sigdelset(&suspmask,SIGALRM);

        /*5 sigsuspned调用期间,采用临时阻塞信号集suspmask奇幻原有阻塞信号集
        *这个信号集中不包括SIGALRM信号,同时挂起等待
        当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集 */
        sigsuspend(&suspmask);

        unslept = alarm(0);
        //6 恢复SIGALRM原有的处理动作,呼应前面注释1
        sigaction(SIGALRM,&oldact,NULL);

        //7 解除对SIGALRM的阻塞,呼应前面注释2
        sigprocmask(SIG_SETMASK,&oldmask,NULL);
        return unslept;

}

int main(void){
        while(1){
                mysleep(2);
                printf("Two seconds passed\n");
        }
        return 0;

}

程序结构:设置信号屏蔽字涉及定义信号sigset_t、初始化信号sigempty()、给信号添加屏蔽字sigaddset()、解除(或屏蔽)信号屏蔽字sigprocmask();产生信号涉及键盘产生如Ctrl c,alarm()函数倒计时结束触发内核向当前进程发送14)SIGALRM信号;

注册一个捕捉信号函数涉及struct sigaction定义newact变量,初始化newact及属性,sigaction()函数等待捕捉信号;

sigsuspend()函数用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集, 这个信号集中不包含SIGALRM信号,同时挂起等待,当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集;

    sigaddset(&newmask, SIGALRM);

    sigprocmask(SIG_BLOCK, &newmask, &oldmask);  //信号屏蔽字mask,返回信号跑屏蔽字oldmask

   /*4.构造一个调用sigsuspend临时有效的阻塞信号集,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    /*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
     *  这个信号集中不包含SIGALRM信号,同时挂起等待,
     *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/  

    sigsuspend(&suspmask); 

小结

竞态条件跟系统负载有紧密关系,体现出信号的不可靠性系统负载越严重,信号不可靠性越强

不可靠由其原理所致信号是通过软件实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理结束后,需要扫描PCB中的未决信号集,来判断是够应处理某个信号。当系统负载过重是,会出现时序混乱

这种意外只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他弥补,且由于该错误不具规律性,后期捕捉和重现十分困难;

全局变量的异步IO

分析下面父子进程交替数数程序,当捕捉函数里的sleep函数取消,程序即会出现问题,分析原因?

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

int n = 0, flag = 0;
void sys_err(char *str)
{
    perror(str);
    exit(1);
}
void do_sig_child(int num)
{
    printf("I am child  %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
void do_sig_parent(int num)
{
    printf("I am parent %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
int main(void)
{
    pid_t pid;
struct sigaction act;

    if ((pid = fork()) < 0)
        sys_err("fork");
    else if (pid > 0) {     
        n = 1;
        sleep(1);
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);             //注册自己的信号捕捉函数  父使用SIGUSR2信号
        do_sig_parent(0);						  
        while (1) {
            /* wait for signal */;
           if (flag == 1) {                         //父进程数数完成
                kill(pid, SIGUSR1);
                flag = 0;                        //标志已经给子进程发送完信号
            }
        }
    } else if (pid == 0) {       
        n = 2;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);

        while (1) {
            /* waiting for a signal */;
            if (flag == 1) {
                kill(getppid(), SIGUSR2);
                flag = 0;
            }
        }
    }
    return 0;
}

例子中,通过flag变量标记程序实行进度,flag置1表示数数完成,flag置0表示给另一个进程发送信号完成。问题出现位置在父子进程kill函数之后需要紧接着调用flag,将其置0,标记信号已经发送,但这期间有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局变量flag的值

如何解决该问题?可以使用“锁”机制,当操作全局变量时,通过加锁,解锁来解决该问题。现阶段在编程期间若使用全局变量,特别需注意全局变量的异步IO可能造成的问题。

可重入函数、不可重入函数

一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”,根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种;

注意

  • 定义可重入函数,函数内不能包含有全局变量及static变量不能使用malloc、free;
  • 信号捕捉函数设计为可重入函数
  • 信号处理程序可调用可重入函数
  • 没有包含上述列表中的函数大多是不可重入的,其原因为:使用静态数据结构、调用malloc、free、是标准I/O函数。

SIGCHLD信号

SIGCHLD产生条件

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

借助SIGCHLD信号回收子进程

子进程结束运行,其父进程会收到SIGCHLD信号,该信号的默认处理动作是忽略,可以捕捉该信号,在捕捉函数中完成子进程状态的回收

#include<stdio.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>

void do_sig_child(){
        int status;
        pid_t pid;
        while((pid = waitpid(0,&status,WNOHANG)) >0){ //WNOHANG:没有子进程存在立即返回
                if(WEXITSTATUS(status))
                        printf("child %d exit %d\n",pid,WEXITSTATUS(status));//WEXITSTATUS:子进程正常终止,返回true
                else if(WIFSIGNALED(status))
                        printf("child %d cancel signal %d\n",pid,WTERMSIG(status));//WTERMSIG:信号导致子进程终止,返回信号编号
        }
}

int main(void){
        pid_t pid;
        int i;
        //1 循环创建子进程,只由父进程创建子进程
        for(i=0; i<10; i++){
                if((pid = fork()) == 0)//判断
                        break;
                else if(pid < 0){
                        perror("fork");
                        exit(1);
                }
        }
        //2 子进程
        if(pid == 0){
                int n=1;
                while(n--){
                        printf("child ID %d\n",getpid());
                        sleep(1);
                }
                return i+1;//返回值给谁?
        }else if(pid > 0){//3 父进程
        //4 定义一个信号注册捕捉函数
                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;
}
  

该例子结合17)SIGCHLD信号默认动作,这里需掌握父进程使用捕捉函数回收子进程方式

若每创建一个子进程后不使用sleep函数可以吗若将程序中捕捉函数内部的while替换为if,是够可行?为什么

可以,但需要在需要在捕捉信号函数前后添加信号屏蔽和解除屏蔽语句,因为有可能在捕捉函数未完成注册已经产生信号,导致捕捉函数无法捕捉信号

sigset_t set,oldset;
int sig;
sig = sigprocmask(SIG_BLOCK,&set,&oldset);//设置屏蔽信号
if(sig < 0){
    perror("sigprocmask error");
    exit(1);
}
/*...*/
sig = sigprocmask(SIG_UNBLOCK,&set,oldset)//设置解除屏蔽
if(sig < 0){
    perror("sigprocmask error");
    exit(1);
}

思考:信号不支持队列,当正执行SIGCHLD捕捉函数时,再过来一个或多个SIGCHLD信号怎么处理?

 if() 回调函数回收完一个子进程就退出了可能多个子进程同时死亡,但常规信号不会记录死亡次数,即在处理函数中,其他子进程终止向父进程发送信号会被忽略(屏蔽)while()回收完一个子进程后继续判断,依次回收子进程,进依次捕捉函数,可以回收多个子进程

子进程结束status处理方式

pid_t waitpid(pid_t pid,int *status,int options)
    options
        WNOHANG
            //没有子进程结束,立即返回
        WUNTRACED
            //如果子进程由于被停止产生的SIGCHLD,waitpid则立即返回
        WCONTINUED
            //如果子进程由于被SIGCONT唤醒而产生的SIGCHLD,waitpid则立即返回
    //获取status
    WIFEXITED(status)
        //子进程正常exit终止,返回真
            WEXITSTATUS(status)返回子进程正常退出值
    WIFSIGNALED(status)
        //子进程被信号终止,返回真
            WTERMSIG(status)//但会终止子进程的信号值
    WIFSTOPPED(status)
        //子进程被停止,返回真
            WSTOPGIG(status)//返回停止子进程的信号值
    WIFCONTINUED(status)

SIGCHLD信号注意问题

  1. 子进程继承父进程信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending;
  2. 注意注册信号捕捉函数的位置
  3. 应该在fork之前,阻塞SIGCHLD信号注册捕捉函数后解除阻塞

信号传参

发送信号传参

sigqueue函数对应kill函数,但可在想指定进程发送信号的同时携带参数

int sigqueue(pid_t pid,int sig,const union sigval value);//成功:0,失败:-1,设置errno
union sigval{
    int sigval_int;
    void *sigval_ptr;
};

向指定进程发送指定信号的同时,携带数据,但注意传地址不同进程之间虚地址空间各自独立,将当前进程地址传递給另一进程没有实际意义

捕捉函数传参

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
//signum:信号编号(宏名)
//act定义新的屏蔽字,oldact:保存旧的屏蔽字,用作恢复之前状态
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_falgs 默认值0)而应使用sa_sigaction。但必须将sa_flags指定为SA_SIGINFOsiginfo_t是一个成员十分丰富的结构体类型,可携带各种与信号相关的数据

信号中断系统调用

系统调用可 分为两类:慢速系统调用和其他系统调用

  1. 慢速系统调用:可使进程永远阻塞的一类,如果在阻塞期间受到一个信号,该系统调用就被中断,不在继续执行(早期),可设定系统调用是否重启。如:read、write、pause、wait...
  2. 其他系统调用:getpid、getppid、fork...

结合pause,回顾慢速系统调用:

慢速系统调用被中断的相关行为,实际上是pause行为

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

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

sa_flags其他参数,如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值