Linux系统编程学习笔记--第六章

6 信号

该节对应第十章——信号。

6.1 前置概念

并发和并行

并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

同步和异步

进程同步:按照一定的顺序协同进行(有序进行),而不是同时。即一组进程为了协调其推进速度,在某些地方需要相互等待或者唤醒,这种进程间的相互制约就被称作是进程同步。这种合作现象在操作系统和并发式编程中属于经常性事件。
例如,在主线程中,开启另一个线程。另一个线程去读取文件,主线程等待该线程读取完毕,那么主线程与该线程就有了同步关系。
进程异步:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

异步事件的处理:查询法(发生频率高)和通知法(发生频率低)

6.2 信号的概念

信号是软件层面的中断,是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断,从它的命名可以看出,它的实质和使用很像中断。

进程之间可以通过调用kill库函数发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。

每个信号都有一个名字。这些名字都以3个字符SIG开头。头文件<signal.h>中,信号名都被定义为正整数常量(信号编号)。

通过命令kill -l可以列出所有可用信号:

信号值 1 ~ 31 为不可靠信号(标准信号),信号会丢失;信号值 34 ~ 64 为可靠信号(实时信号),信号不会丢失。

[root@zoukangcheng signal]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

信号名            信号值    发出信号的原因
SIGHUP            1    终端挂起或者控制进程终止
SIGINT              2    键盘中断Ctrl+c
SIGQUIT           3    键盘的退出键被按下
SIGILL               4    非法指令
SIGABRT          6    由abort(3)发出的退出指令
SIGFPE             8    浮点异常
SIGKILL            9    采用kill -9 进程编号 强制杀死程序。
SIGSEGV        11    无效的内存引用
SIGPIPE          13    管道破裂:写一个没有读端口的管道
SIGALRM        14    由alarm(2)发出的信号
SIGTERM    15    采用“kill 进程编号”或“killall 程序名”通知程序。
SIGUSR1    30,10,16    用户自定义信号1
SIGUSR2    31,12,17    用户自定义信号2
SIGCHLD    20,17,18    子进程结束信号
SIGCONT    19,18,25    进程继续(曾被停止的进程)
SIGSTOP    17,19,23    终止进程
SIGTSTP    18,20,24    控制终端(tty)上按下停止键
SIGTTIN    21,21,26    后台进程企图从控制终端读
SIGTTOU    22,22,27    后台进程企图从控制终端写

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核”在此信号发生时,请执行下列操作”。

当某个信号出现时,可以告诉内核按下列三种方式之一进行处理,称之为信号的处理或与信号相关的工作:

忽略此信号
捕捉信号
执行系统默认工作
下图列出了所有信号的名字,说明了哪些系统支持此信号和信号对应的系统默认工作。可以看出C标准库支持的信号是最少的。大部分的信号的默认操作是终止进程。

core是在进程当前工作目录的core文件中复制了该进程的内存映像,core文件记录了进程终止时的错误报告,大多数UNIX系统调试程序都使用core文件检查进程终止时的状态。

补充:kill命令

kill [参数] [进程号]

常用参数:

-l:信号编号
如果是kill -l,则列出全部的信号名称
如果是kill -信号编号 pid,则将该编号对应的信号发送给指定pid的进程
默认为15,对应发出终止信号,例如kill 23007


其他常用编号:

信号编号信号名含义
0EXIT程序退出时收到该信息
1  HUP挂掉电话线或终端连接的挂起信号,这个信号也会造成某些进程在没有终止的情况下重新初始化
2INT 表示结束进程,但并不是强制性的,常用的 “Ctrl+C” 组合键发出就是一个 kill -2 的信号
QUIT 退出
9KILL 杀死进程,即强制结束进程
11SEGV 段错误
15TERM正常结束进程,是 kill 命令的默认信号

        
 

6.3 signal函数

补充:typedef的用法总结

定义一种类型的别名:基本用法

typedef char* PCHAR;
PCHAR pa, pb;
typedef struct tagPOINT {
    int x;
    int y;
}POINT;

POINT p1, p2;

定义与平台无关的类型:当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。标准库就广泛使用了这个技巧,比如size_t,pid_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏更稳健。

typedef long double REAL; // 平台1
typedef double REAL; // 平台2
// ...

为复杂声明定义一个新的简单的别名。

例如:

// 相当于给返回值为int,形参为int的这一类函数起了一个别名FUNC
typedef int FUNC(int);

// 定义了返回值为int,形参为int的函数f
FUNC f;  // 相当于:int f(int);

int a = f(1); // 使用
// 给返回值为int*,形参为int的指针函数(指针函数:返回指针的函数,本质是函数;函数指针:指向函数的指针,本质是指针)起了一个别名FUNCP,本质是一个函数
typedef int* FUNCP(int);

FUNCP p; // 相当于 int* p(int);

int* a = p(1); // 使用
// 给指向返回值为int*,形参为int的指针函数的指针其一个别名FUNCP,本质是一个指针
typedef int* (*FUNCP)(int);

FUNCP p; // 相当于 int* (*p)(int);

int* a = (*p)(1); // 使用

又如:

// 原始声明,该声明本质上是一个函数指针
void (* signal(int signum, void (*func)(int)))(int);

// 最外层是一个函数指针,形式为:
void (*)(int);

// 内层是一个返回指针,形参为int,函数指针的函数signal
// 注意到signal的形参也有一个函数指针void (*func)(int),这两个函数指针指向的都是同一种函数,即返回值为void,形参为int
void (*)(int);

// 因此给公共部分起一个别名sighandler_t,sighandler_t本质上是一个指针
typedef void (*sighandler_t)(int);

// 简化后的形式:
sighandler_t signal(int signum, sighandler_t func);

UNIX系统信号机制最简单的接口是signal函数,函数原型如下:

// CONFORMING TO C89, C99, POSIX.1-2001.
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

// 注意,typedef没有定义在头文件中,因此必须要写出,否则按照下面的形式给出signal函数,APUE上的就是这种形式:

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

signum参数是上图中的信号名,常用宏名来表示,例如SIGINT
func参数是下面的一种:
SIG_IGN:向内核忽略此信号,除了SIGKILL和SIGSTOP
SIG_DFL:执行系统默认动作
当接到此信号后要调用的函数的地址:在信号发生时,调用该函数;称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数
作用:signal函数为signum所代表的信号设置一个信号处理程序func,换句话说,signal就是一个注册函数。

代码示例1

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(void) {
    int i;
    // 忽略SIGINT信号
    signal(SIGINT, SIG_IGN);
    for(i = 0; i < 10; i++) {
        // 每1s向终端打印1个*
        write(1, "*", 1);
        sleep(1);
    }
    exit(0);
}

信号SIGINT产生的方式就是快捷键CTRL + C

代码示例2

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

static void int_handler(int s) {
    // 向终端打印!
    write(1, "!", 1);
}

int main(void) {
    int i;
    // 函数名就是函数的地址
    signal(SIGINT, int_handler);
    for(i = 0; i < 10; i++) {
        write(1, "*", 1);
        sleep(1);
    }
    exit(0);
}

每按1次CTRL + C,终端就打印1个!

代码示例3——阻塞和非阻塞

上述程序,如果一直按着CTRL + C,程序会小于10S就会结束。

原因在于:信号会打断阻塞的系统调用。这里的阻塞是write和sleep函数。

分析:进程运行到sleep(1)的时候,由运行态进入阻塞态,此时如果有信号到来,例如SIGINT,会打断阻塞(唤醒进程),让进程进入就绪态,获得时间片进入运行态,此时进程还没阻塞到1s,就进入了就绪态,即信号会打断阻塞的系统调用。

阻塞:为了完成一个功能,发起一个调用,如果不具备条件的话则一直等待,直到具备条件则完成
非阻塞:为了完成一个功能,发起一个调用,具备条件直接输出,不具备条件直接报错返回

此前学习过的所有IO函数,都是阻塞IO,即阻塞的系统调用。

以open为例,进程调用open时,进入阻塞态,等待IO设备打开,如果IO设备打开时间过长,此时有一个信号到来,就会打断open调用,使其打开设备失败。

因此,在设备打开失败的时候,需要判断是因为open自身引发的错误,还是因为信号打断而没有打开,对于前者,以以往的方式处理错误,而对于后者应该尝试再次打开设备,而不是报错后退出程序。

注意:对于所有的阻塞系统调用,都要处理是因为自身调用出现的真错,还是因为信号中断导致的假错。

在宏中,有一个名为EINTR的errno,即为被信号中断而引发的错误。当进程在执行一个阻塞的系统调用时捕捉到一个信号,则被中断不再执行该系统调用,该系统调用返回错误,errno就会被设置为EINTR。

以前面的一个程序为例,修改后的代码为:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFSIZE 1024 // 缓冲区大小

int main(int argc, char **argv) {
    int sfd, dfd;
    char buf[BUFSIZE];
    int len, ret, pos;

    if(argc < 3) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }
	
    do {
        if((sfd = open(argv[1], O_RDONLY)) < 0) {
            if(errno != EINTR) {
                // 不是信号导致的错误,就退出
                perror("open()");
                exit(1);
            }
        }
    } while(sfd < 0)
	
    do {
        if((dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {			
            if(errno != EINTR) {
                close(sfd);
                perror("open()");
                exit(1);
            }
    	}
    }while(dfd < 0)
    	

    while(1) {
        if((len = read(sfd, buf, BUFSIZE)) < 0) {
            if(errno == EINTR)
                continue;
            perror("read()");
            break;
        }
		
        if(len == 0)
            break;

        pos = 0;
        while(len > 0) {
            if((ret = write(dfd, buf + pos, len)) < 0) {
                if(errno == EINTR)
                    continue;
                perror("write()");
                exit(1);
            }
            pos += ret;
            len -= ret;
        }
    }
    close(dfd);
    close(sfd);

    exit(0);
}

6.4 不可靠信号

信号处理程序由内核调用,在执行该程序时,内核为该处理程序布置现场,此时如果又来一个信号,内核再次调用信号处理程序,可能会冲掉第一次调用布置的现场。


6.5 可重入函数

可重入函数(即可以被中断的函数)可以被一个以上的任务调用,而不担心数据破坏。可重入函数在任何时候都可以被中断,而一段时间之后又可以恢复运行,而相应的数据不会破坏或者丢失。

不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。I/O代码通常不是可重入的,因为它们依赖于像磁盘这样共享的,单独的资源。

所有的系统调用都是可重入的,一部分库函数也是可重入的,例如memcpy。

函数不可重入的条件

函数内使用了静态的数据。
函数内使用了malloc或者free函数
函数内调用了标准的I/O函数
例如:

int temp;
void swap(int *ex1, int *ex2) {
    temp = *ex1; //(1)
    *ex1 = *ex2;
    *ex2 = temp;
}

分析:该函数中的全局变量temp是的函数变成了一个不可重入的函数,因为在多任务系统中,假如在任务1中调用swap函数,而程序执行到(1)处时被中断,进而执行其它的任务2,而刚好任务2也调用了swap函数,则temp里存的值则会被任务2改变。从而回到任务1被中断处继续执行的时候,temp里存的值已经不再是原来存的temp值了,进而产生了错误。

使得函数可重入的方法

不要使用全局变量,防止别的代码覆盖这些变量的值。
调用这类函数之前先关掉中断,调用完之后马上打开中断。防止函数执行期间被中断进入别的任务执行。
使用信号量(互斥条件)

6.6 信号的响应过程

补充:用户态和内核态的切换

Linux系统中的内核态本质是内核,是一种特殊的软件程序,用于控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。0-4G范围的虚拟空间地址都可以操作,尤其是对3-4G范围的高位虚拟空间地址必须由内核态去操作。

用户态提供应用程序运行的空间,为了使应用程序访问到内核管理的资源,例如CPU,内存,I/O等。用户态只能受限的访问内存,且不允许访问外设(硬盘、网卡等);内核态CPU可以访问内存所有数据,包括外设,且可以将自己从一个程序切换到另一个程序。

用户态切换到内核态的三种方式:

发生系统调用时:(主动)这是处于用户态的进程主动请求切换到内核态的一种方式。用户态的进程通过系统调用申请使用操作系统提供的系统调用服务例程来处理任务。而系统调用的机制,其核心仍是使用了操作系统为用户特别开发的一个中断机制来实现的,即软中断。
产生异常时:(被动)当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行的进程切换到处理此异常的内核相关的程序中,也就是转到了内核态,如缺页异常。
外设产生中断时:(被动)当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作的完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

信号在内核中的表示

信号递达(Delivery):实际执行信号处理的动作。
信号未决(Pending):信号从产生到递达之间的状态。
信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

有如下结论:

信号从收到到响应有一个不可避免的延迟。
标准信号的响应没有严格顺序。
内核为每个进程都维护了两个位图,一般为32位,两个位图进行与操作:

信号屏蔽字 mask :用来屏蔽信号,mask初始值一般全部都是1,表示不屏蔽全部信号
位图 pending: 用来记录当前进程收到哪些信号,一般初始值全部都是0,表示没有收到信号,未决信号集。

信号响应过程

以打印*和!的程序为例。

在没有其他中断的情况下,当时间片耗尽时,进程被计时器中断,使得该进程被切换至内核态,在内核态中运行计时器中断的处理函数(handler),进程调度算法就发生在计时器中断的处理函数中(在这个处理函数中,系统会根据任务调度算法,从就绪队列里选择下一个运行的进程)。该进程等待调度。
进程当前的状态和信息被压入到内核栈中,其中包括程序的返回地址。
进程被内核调度时,进程从内核态切换至用户态,从内核栈中保存的返回地址继续运行
进程从内核态切换至用户态时(也只有在这个时刻),会执行mask & pending,确认自己是否收到信号。对于这种情况,按位与结果为每位都为0,表示进程没有收到信号。
当进程在某一刻收到SIGINT信号,此时将pending第2位置为1,表示收到SIGINT信号,但此时进程并不会被该信号中断(即响应该信号)。当时间片耗尽,一直到被调度,进程从内核态切换至用户态,执行mask & pending时,发现只有第2位结果为1,此时进程才响应该信号。因此说,信号从收到到响应有一个不可避免的延迟。
响应信号时的操作:内核将该位的mask和pending都置为0,并将程序返回地址修改为int_handler,即信号响应程序的地址,此时在用户态执行信号响应程序。换句话说,信号的收到的时刻是随机的,而信号的响应总是在从内核态切换到用户态时进行的;
信号响应程序执行完毕后,从用户态切换至内核态,内核将返回地址修改为程序的返回地址,并将mask的该位置为1;
再次被调度到时,切换至用户态,执行mask & pending,发现均为0,说明刚才的信号已经被响应了,则继续向下执行程序;

如何忽略一个信号?

注意到func(信号处理程序)可以为宏SIG_ING,实质上是将mask的位置为0,表示屏蔽该信号。

注意:我们能做的是不响应信号,即不对信号做处理(屏蔽),而不是阻止信号产生和到来。信号的随时都会到来,将pending置为1。

标准信号为什么不可靠,或者说标准信号为什么会丢失?

信号响应时,位图为(m0 p0),此时又来多个信号,则p被反复置为1,即结果总是为(m0 p1);信号响应完毕时,将m置为1,此时为(m1 p1);返回用户态时,发现该位仍然为1,说明又来一个信号(注意不是多个),则继续响应…所以多个信号到来时,只有最后一个信号能够被响应,前面的信号都被丢失了。

总结:mask和pending的变化情况

mask pending
1		0	// 初始化
			// 进程处理自己的任务...
1		1	// 某一时刻信号到来
            // 进程处理自己的任务...,直到重新被调度时
0		0	// 信号响应 内核态->用户态时
    		// 进入到信号处理函数中执行
1		0	// 信号响应完毕
     		// 进程继续处理自己的任务

6.7 常用函数 I

6.7.1 kill

kill函数用于向进程发送信号,注意不是杀死进程。

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

int kill(pid_t pid, int sig);

参数:

pid:向哪个进程发送信号
pid > 0:发送信号给指定进程
pid = 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程,相当于组内广播。
pid < -1:发送信号给该绝对值所对应的进程组id的所有组员,相当于组内广播。
pid = -1:发送信号给所有权限发送的所有进程。

sig:待发送的信号
sig = 0:没有信号发送,但会返回-1并设置errno,用来检测某个进程id或进程组id是否存在。注意返回-1时并不能表明该id不存在,而是要根据errno来判断,详见下面的返回值。


返回值:

成功返回0
失败返回-1,并设值errno
EINVAL:无效的信号sig
EPERM:调用进程没有权限给pid的进程发送信号
ESRCH:进程或进程组不存在

 6.7.2 raise

raise函数用于向调用进程发送信号,即自己给自己发送信号。

#include <signal.h>

int raise(int sig);

// 相当于
kill(getpid(), sig);

6.7.3 alarm

alarm函数

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

作用:设置定时器。在指定seconds后,内核会给当前进程发送SIGALRM信号(定时器超时)。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器,所以多个alarm函数共同调用时,后面设置的时钟会覆盖掉前面的时钟。

返回值:返回0或剩余的秒数,无失败。

代码示例

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

int main(void) {
    alarm(5);
    while(1);
    exit(0);
}

执行结果:

[root@zoukangcheng signal]# ./alarm 
Alarm clock

6.7.4 pause

pause函数用于等待信号。

#include <unistd.h>

int pause(void);

进程调用pause函数时,会造成进程主动挂起(处于阻塞状态,并主动放弃CPU),并且等待信号将其唤醒。

代码示例

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

int main(void) {
    alarm(5);
    while(1)
        pause();
    exit(0);
}

当调用到pause()时,该进程挂起,此时不再占用CPU,5s过后,接收到SIGALRM信号,采取默认动作终止。

信号的处理方式有三种:

默认动作
忽略处理
捕捉


进程收到一个信号后,会先处理响应信号,再唤醒pause函数。于是有下面几种情况:

如果信号的默认处理动作是终止进程,则进程将被终止,也就是说一收到信号进程就终止了,pause函数根本就没有机会返回,例如上面的例子
如果信号的默认处理动作是忽略,则进程将直接忽略该信号,相当于没收到这个信号,进程继续处于挂起状态,pause函数不返回
如果信号的处理动作是捕捉,则进程调用完信号处理函数之后,pause返回-1,errno设置为EINTR,表示“被信号中断”
pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒
sleep = alarm + pause

代码示例

需求:让程序等待5s

使用time

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

int main(void) {
    time_t end;
    int64_t count = 0;

    end = time(NULL) + 5;

    while(time(NULL) <= end) {
        count++;
    }

    printf("%lld\n", count);

    exit(0);
}

执行结果:

[root@zoukangcheng signal]# time ./pause > /tmp/out
real    0m5.142s
user    0m4.950s
sys     0m0.005s
[root@zoukangcheng signal]# time ./pause > /tmp/out
real    0m5.181s
user    0m5.045s
sys     0m0.008s

使用alarm

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

// 循环跳出的标志
static int loop = 1;

static void alrm_handler(int s) {
    loop = 0;
}

int main(void) {
    int64_t count = 0;
    // 对信号SIGALRM注册信号处理函数
    signal(SIGALRM, alrm_handler);
    alarm(5);
    while(loop)
        count++;
    printf("%lld\n", count);
    exit(0);
}

运行结果:

[root@zoukangcheng signal]# time ./5sec_sig > /tmp/out
real    0m5.029s
user    0m4.821s
sys     0m0.012s
[root@zoukangcheng signal]# time ./5sec_sig > /tmp/out
real    0m5.017s
user    0m4.864s
sys     0m0.006s

当对上述程序进行编译优化时:

gcc 5sec_sig.c -O1

再次执行,发现程序一直在运行。

原因:编译优化时,gcc看到下面的循环体中没有出现loop,所以gcc认为loop是不变的,将loop的值存储在高速缓存(CPU的寄存器)中,每次读取loop的值时,从高速缓存中读取,而不是在loop实际存放的内存地址中读取,因此一直在此处循环。

while(loop)
    count++;

解决方法:用volatile修饰loop,此时编译器认为该变量易变,每次读取时从实际内存地址中读取:

static volatile int loop = 1;

未加易变修饰前:(省略不重要的代码)

main:
.LFB24:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $5, %edi
        movl    $0, %eax
        call    alarm
        movl    $alrm_handler, %esi
        movl    $14, %edi
        call    signal
        cmpl    $0, loop(%rip) # 判断loop
        jne     .L5 # 跳转到循环体中
        movl    $0, %esi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %edi
        call    exit
.L5:
        jmp     .L5 # 死循环
        .cfi_endproc

加上易变修饰后:

main:
.LFB24:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $5, %edi
        movl    $0, %eax
        call    alarm
        movl    $alrm_handler, %esi
        movl    $14, %edi
        call    signal
        movl    loop(%rip), %eax
        testl   %eax, %eax # 判断loop
        je      .L5 # 跳入循环
        movl    $0, %esi
.L4:
        addq    $1, %rsi
        movl    loop(%rip), %eax
        testl   %eax, %eax
        jne     .L4
        jmp     .L3
.L5:
        movl    $0, %esi
.L3:
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %edi
        call    exit
        .cfi_endproc

6.7.5 漏桶

leaky bucket也叫漏桶,就是将请求先放到一个桶中进行排队,然后按一个固定的速率来处理请求,即所谓的漏出。

桶具有一定的容量,即最大容量的请求数,当排队请求的数量超过桶的容量时,再进来的请求就直接过滤掉,不再被处理。换句话说就是请求先在桶中排队,系统或服务只以一个恒定的速度从桶中将请求取出进行处理。如果排队的请求数量超过桶能够容纳的最大数量,即桶装满了,则直接丢弃。

漏桶算法(Leaky Bucket)是网络世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。

需求:实现一个复制文本到标准输出的程序,要求10字符10字符的复制,且不能让CPU空转。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

#define CPS     10
#define BUFSIZE CPS

static volatile int loop = 0;

static void alrm_handler(int s) {
    alarm(1);
    loop = 1;
}

int main(int argc, char **argv) {
    int sfd, dfd = 1;
    char buf[BUFSIZE];
    int len, ret, pos;

    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    signal(SIGALRM, alrm_handler);
    alarm(1);
	
    do {
        if((sfd = open(argv[1], O_RDONLY)) < 0) {
            if(errno != EINTR) {
                perror("open()");
                exit(1);
            }
        }
    } while(sfd < 0);
	
    while(1) {

        while(!loop)
            // 防止CPU空转
            pause();
        loop = 0;
        while((len = read(sfd, buf, BUFSIZE)) < 0) {
            if(errno == EINTR)
                continue;
            perror("read()");
            break;
        }
		
        if(len == 0)
            break;

        pos = 0;
        while(len > 0) {
            if((ret = write(dfd, buf + pos, len)) < 0) {
                if(errno == EINTR)
                    continue;
                perror("write()");
                exit(1);
            }
            pos += ret;
            len -= ret;
        }
    }
    close(sfd);
    exit(0);
}

6.7.6 令牌桶

系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
令牌桶和漏桶的区别

主要区别在于漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。

6.8 信号集

信号集是一个能够表示多个信号的数据类型。

POSIX.1定义数据类型sigset_t来表示一个信号集(本质为整型),并且定义了下列5个处理信号集的函数:

#include <signal.h>

// 置空一个信号集
int sigemptyset(sigset_t *set);

// 填充满一个信号集
int sigfillset(sigset_t *set);

// 将一个信号加入信号集
int sigaddset(sigset_t *set, int signum);

// 将一个信号从信号集删除
int sigdelset(sigset_t *set, int signum);

// 上述函数成功返回0,失败返回-1

// 检查一个信号集中是否有这个信号,存在返回1,不存在返回0
int sigismember(const sigset_t *set, int signum);

6.9 sigprocmask函数

sigprocmask函数可以检测或更改,或同时进行检测和更改进程的信号屏蔽字(阻塞信号集)。注:对信号来说,阻塞和屏蔽是一个意思。

函数原型:

#include <signal.h>
// 成功返回0,失败返回-1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how:用于指定信号修改的方式,有三种选择:
SIG_BLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。即set包含了希望阻塞的附加信号;
SIG_UNBLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。即set包含了希望解除阻塞的信号;
SIG_SETMASK:该进程新的信号屏蔽是set指向的值;
*set:和how结合使用
*oldset:进程的当前信号屏蔽字通过oset返回(oldset),如果不关心旧的信号集设置,可设置为NULL


代码示例1

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

static void int_handler(int s) {
	write(1, "!", 1);
}

int main(void) {
	int i;
	sigset_t set;

	signal(SIGINT, int_handler);
    // 置空一个信号集set
    sigemptyset(&set);
    // 将SIGINT加入到信号集set中
    sigaddset(&set, SIGINT);
    while(1) {
        // 设置信号屏蔽字
        // 将SIGINT屏蔽
        sigprocmask(SIG_BLOCK, &set, NULL);
        for(i = 0; i < 5; i++) {
            write(1, "*", 1);
            sleep(1);
	    }
        write(1, "\n", 1);
        // 打印5个*后,将SIGINT解除屏蔽
        sigprocmask(SIG_UNBLOCK, &set, NULL);
    }
	exit(0);
}

执行结果:

[root@zoukangcheng signal]# ./block
*****
*^C****
!*^C^C^C^C*^C^C^C^C^C^C^C^C^C***
!***^C^C^C**
!****^\Quit

解析:第三行发送SIGINT信号时,SIGINT的屏蔽字位为0,pending位置为1,当打印完成后,解除屏蔽,进程响应该信号,在第四行打印了!;第四行多次发送SIGINT信号,也只是对pending位反复置为1,在第五行也只打印一次!;

代码示例2

int main(void) {
	int i;
	sigset_t set, oset;
	signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    while(1) {
        // 将SIGINT阻塞,并将原始的屏蔽字存储在oset中,且原始屏蔽字都是1
        sigprocmask(SIG_BLOCK, &set, &oset);
        for(i = 0; i < 5; i++) {
            write(1, "*", 1);
            sleep(1);
	    }
        write(1, "\n", 1);
        // 将屏蔽字设置为oset信号集的值,即全1,没有屏蔽
        sigprocmask(SIG_SETMASK, &oset, NULL);
    }
	exit(0);
}

功能和代码示例1相同。

代码示例3

为了保证在进入模块和退出模块时进程的信号屏蔽字是一致的,需要修改代码为:

int main(void) {
	int i;
	sigset_t set, oset, saveset;
	signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    // 将进程起始的信号屏蔽字保存在saveset中
    sigprocmask(SIG_UNBLOCK, &set, &saveset);
    while(1) {
        sigprocmask(SIG_BLOCK, &set, &oset);
        for(i = 0; i < 5; i++) {
            write(1, "*", 1);
            sleep(1);
	    }
        write(1, "\n", 1);
        sigprocmask(SIG_SETMASK, &oset, NULL);
    }
    // 退出时,还原信号屏蔽字
    sigprocmask(SIG_SETMASK, &saveset, NULL);
	exit(0);
}

6.10 常用函数II

6.10.1 sigsuspend函数

该函数通过将进程的屏蔽字替换为由参数mask给出的信号集,然后将进程挂起(阻塞)。

#include <signal.h>

int sigsuspend(const sigset_t *mask);

功能描述上和pause函数相同,即在等待信号的时候让进程挂起。

区别:sigsuspend实际是将sigprocmask和pause结合起来原子操作。

代码示例

需求:先打印一排*,等待信号,然后打印一排*,以此类推。

使用pause

static void int_handler(int s) {
	write(1, "!", 1);
}

int main(void) {
	int i;
	sigset_t set, oset, saveset;
	signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_UNBLOCK, &set, &saveset);
    while(1) {
        sigprocmask(SIG_BLOCK, &set, &oset);
        for(i = 0; i < 5; i++) {
            write(1, "*", 1);
            sleep(1);
	    }
        write(1, "\n", 1);
        sigprocmask(SIG_SETMASK, &oset, NULL);
        pause();
    }
    sigprocmask(SIG_SETMASK, &saveset, NULL);
	exit(0);
}

执行结果:

[root@zoukangcheng signal]# ./susp 
*****
^C!*****
^C!**^C^C^C***
!

第二行开始处:当内层循环执行完毕后,到pause处挂起,此时SIGINT到来,首先处理信号,然后唤醒进程,继续执行内层循环打印。

注意第四行,当在内层循环执行时,有多个SIGINT信号到来,由于被屏蔽,所以不打印叹号,打印星号结束后,代码第十九行解除屏蔽(注意,代码第十九行和第二十行之间也会存在多个中断),响应信号(第五行的第一个叹号),再执行到代码第二十行处时,此时没有信号到来,所以一直挂起。

使用sigsuspend

static void int_handler(int s) {
	write(1, "!", 1);
}

int main(void) {
	int i;
	sigset_t set, oset, saveset;
	signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    // 保存初始屏蔽字
    sigprocmask(SIG_UNBLOCK, &set, &saveset);
    // 设置阻塞
    sigprocmask(SIG_BLOCK, &set, &oset);
    while(1) {
        for(i = 0; i < 5; i++) {
            write(1, "*", 1);
            sleep(1);
	    }
        write(1, "\n", 1);
        // 挂起
        sigsuspend(&oset);
        /*
        sigset_t tmpset;
        sigprocmask(SIG_SETMASK, &oset, &tmpset);
        pause();
        sigprocmask(SIG_SETMASK, &tmpset, NULL);
        */
    }
    sigprocmask(SIG_SETMASK, &saveset, NULL);
	exit(0);
}

6.10.2 sigaction函数

sigaction函数是升级版的signal函数。

sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)。

#include <signal.h>

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

signum参数指出要捕获的信号类型
act参数指定新的信号处理方式
oldact参数输出先前信号的处理方式(如果不为NULL的话)


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:此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask:用来设置在处理该信号时暂时将 sa_mask 指定的信号集屏蔽
sa_flags:用来设置信号处理的其他相关操作,下列的数值可用。
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息
sa_sigaction:这是一个三个参数的sa_handler函数版本。如果设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。其中参数siginfo_t是一个结构体类型

代码示例1

// 需求:三种信号SIGQUIT,SIGTERM,SIGINT共用一个信号处理函数,且该函数中释放了重要的资源,因此不可重入。

static void daemon_exit(int s) {
    // 释放相关资源
    // ...
    exit(0);
}

int main() {
    struct sigaction sa;
    // 设置信号处理函数
    sa.sa_handler = daemon_exit;
    sigemptyset(&sa.sa_mask);
    // 将SIGQUIT,SIGTERM,SIGINT信号阻塞,防止重入
    sigaddset(&sa.sa_mask, SIGQUIT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGINT);
    sa.sa_flags = 0;
    
    // 要捕获SIGQUIT,SIGTERM,SIGINT三种信号
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    // ...
    exit(0);
}

6.11 实时信号

在Linux系统中,信号是一种通信机制,用于在程序执行过程中传递异常或中断事件。它们分为实时信号(Real-time signals)和标准信号(Standard signals)两种类型。

  1. 标准信号(也称为POSIX信号):

    • 标准信号是预定义的一组信号,由内核提供,并可以通过系统调用来发送给进程。这些信号包括SIGINT(Ctrl+C)用于中断,SIGTERM(kill命令的默认信号)用于终止进程,SIGKILL(无法被处理的强制终止)等。
    • 标准信号通常是非阻塞的,接收者可以选择忽略、捕获(处理)或默认处理这些信号。
    • Linux提供了信号处理机制,比如使用sigaction函数来设置信号处理器和行为。
  2. 实时信号(Real-time signals, 或称为SCHED_rt signals):

    • 实时信号主要在实时操作系统(RTOS)中使用,它们是为了满足对响应时间和确定性的高要求而设计的。
    • Linux的实时信号不同于标准信号,它们在调度级别优先级更高,且具有更低的延迟。
    • 实时信号的数量较少,例如SIGRTMIN和SIGRTMAX系列,通常用于任务调度、中断处理等需要严格时间控制的场景。

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为34 ~ 64的信号是后来扩充的,称做可靠信号(实时信号)

实时信号具有排队,不丢失的特点,标准信号会丢失,例如短时间内接收多个标准信号,只会响应一次。

代码示例

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

#define MYRTSIG (SIGRTMIN+6)

static void mysig_handler(int s){
        write(1,"!",1);
}
//需求:当有信号响应时,希望在两行*号之间响应,而不是在*一行中响应

int main()
{
        int i,j;
        sigset_t set,oset;
        //signal(SIGINT,SIG_IGN);
        //当收到信号后,会执行相应的动作
        signal(MYRTSIG,mysig_handler);
        sigemptyset(&set);//将set集合置空
        sigaddset(&set,MYRTSIG);        
        sigprocmask(SIG_BLOCK,&set,&oset);//保留原来的状态,事后恢复

        for(j=0;j<1000;j++)
        {
                for(i=0;i<5;i++)
                {
                        write(1,"*",1);
                        sleep(1);
                }
                write(1,"\n",1);
                sigsuspend(&oset);
                //sigprocmask(SIG_SETMASK,&oset,NULL);
                //pause();//pause和上面的sigprocmask不是原子操作
        }
        exit(0);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值