Linux环境高级编程-信号

本文详细解释了Unix系统中的信号概念,包括信号的分类、发送与捕获、signal函数的作用以及常用信号处理函数如kill、raise、alarm和pause。讨论了信号的响应过程和默认动作,并通过示例展示了如何使用这些函数来控制进程行为。
摘要由CSDN通过智能技术生成

1 信号

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

1.1 前置概念

1.2 信号的概念

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

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

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

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

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

lei@ubuntu:~$ 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	

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

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

  • 忽略此信号
  • 捕捉信号
  • 执行系统默认工作

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

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

补充:kill命令

kill [参数] [进程号]

常用参数:

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

1.3 signal函数

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

作用:
    signal函数为signum所代表的信号设置一个信号处理程序func,换句话说,signal就是一个注册函数。

参数:
    signum参数是上图中的信号名,常用宏名来表示,例如SIGINT
    func参数是下面的一种:
        SIG_IGN:向内核忽略此信号,除了SIGKILL和SIGSTOP
        SIG_DFL:执行系统默认动作
        当接到此信号后要调用的函数的地址:在信号发生时,调用该函数;
             称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数
返回值:
    成功返回以前的信号处理配置
    失败返回 SIG_ERR

代码示例1

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

int main()
{
    int i = 0;
    //忽略SIGINT信号(Ctrl+c)
    signal(SIGINT, SIG_IGN);

    for (i = 0; i < 10; i++)
    {
        //每秒向终端打印一个*
        write(1, "*", 1);
        sleep(1);
    }
        
    return 0;
}

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

执行结果:

lei@ubuntu:~/Desktop$ ./a.out 
***^C*^C*^C*^C*^C*^C^C^C^C^C**lei@ubuntu:~/Desktop$ 

代码示例2

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

static void sig_handler(int a)
{
    //向终端打印!
    write(1, "!", 1);

    return;
}

int main()
{
    int i = 0;
    //函数名就是函数的地址
    signal(SIGINT, sig_handler);

    for (i = 0; i < 10; i++)
    {
        //每秒向终端打印一个*
        write(1, "*", 1);
        sleep(1);
    }
        
    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop$ ./a.out 
***^C!**^C!*^C!*^C!*^C!*^C!*^C!lei@ubuntu:~/Desktop$ 

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

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

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

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

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

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

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

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

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

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

  EINTR  While  blocked  waiting  to  complete  an  open of a slow device
              (e.g., a FIFO; see fifo(7)), the call was interrupted by a  sig‐
              nal handler; see signal(7).
(来自 man 2 open)
  EINTR  The call was interrupted by a signal before any data  was  read;
              see signal(7).
 (来自 man 2 read)

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

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

int main(int argc, char const *argv[])
{
    int fd1 = 0;
    int fd2 = 0;
    ssize_t nret = 0;
    char buff[4096] = {0};

   do {
        fd1 = open("1.jpg", O_RDONLY, 0664);
        if (-1 == fd1)//真错,退出
        {
            perror("fail to open fd1");
            return -1;
        }
        if (errno == EINTR)//假错,重新打开
        {
            continue;
        }
    }while(fd1 < 0);
    
    do {
        fd2 = open("2.jpg", O_WRONLY | O_TRUNC | O_CREAT, 0664);
        if (-1 == fd2)
        {
            perror("fail to open fd2");
            return -1;
        }
        if (errno == EINTR)//假错,重新打开
        {
            continue;
        }
    }while(fd2 < 0);
    

    while(1)
    {
        nret = read(fd1, buff, sizeof(buff));
        if (nret <= 0)
        {
            if (errno == EINTR)
                continue;
            break;
        }
        nret = write(fd2, buff, nret);
        if (nret <= 0)
        {
            if (errno == EINTR)
                continue;
            break;
        }
        printf("nret = %ld\n", nret);
        /*
        if (nret < sizeof(buff))
        {
            break;
        }
        */
    }

    close(fd1);
    close(fd2);

    return 0;
}

1.4 不可靠的信号

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

1.5 可重入函数

1.6 信号的响应过程

1.7 常用函数Ⅰ

1.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:进程或进程组不存在

1.7.2 raise

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

#include <signal.h>

int raise(int sig);

// 相当于
kill(getpid(), sig);
1.7.3 alarm
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

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

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

代码示例

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

int main()
{
    alarm(5);
    alarm(2);
    alarm(3);
    while(1);

    exit(0);
}

执行结果:定时三秒,执行默认动作终止进程。

lei@ubuntu:~/Desktop/sig$ ./a.out 
Alarm clock
1.7.4 pause

pause函数用于等待信号。

#include <unistd.h>

int pause(void);

返回值:
    -1,errno设置为EINTR

进程调用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);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值