Linux系统编程-(四)信号

一.信号概述

1.1 中断

中断就是字面的意思,譬如正在打游戏,手机响了,这时后中断游戏,去接手机,回来再打游戏,这就是中断。

1.2 什么是信号

信号是软件中断,是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号是 Linux 进程间通信的最古老的方式,也是最常用的通信方式。

1.3 信号机制

进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行,是一种异步模式。

1.4 信号状态

三种状态,产生、未决和递达。
1 产生
a)按键产生,如:Ctrl+c、Ctrl+z
b)系统调用产生,如:kill、raise、abort
3)软件条件产生,如:定时器alarm
4)硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
5)命令产生,如:kill命令
2 未决
没有被处理,产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
3 递达
递送并且到达进程,信号被处理了

1.5 查看信号

1)kill -l

(base) zhaow@zhaow-610:~$ 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
//  信号编号)信号名字    

2)man 7 signal

SIGNAL(7)                  Linux Programmer's Manual                 SIGNAL(7)

NAME
       signal - overview of signals

DESCRIPTION
       Linux  supports both POSIX reliable signals (hereinafter "standard sig‐
       nals") and POSIX real-time signals.

......
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
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating-point exception
......

信号的四个要素:
1) 信号的编号

使用kill -l命令可以查看当前系统有哪些信号,不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。

2) 信号的名称
3) 产生信号的事件
4) 信号的默认处理动作

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

1.6 阻塞信号集和未决信号集

Linux内核的进程控制块PCB是一个结构体,这个结构体里面包含了信号相关的信息,主要有阻塞信号集和未决信号集。

信号集 说明
阻塞信号集 将某些信号加入集合,对他们设置屏蔽,当屏蔽信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。
未决信号集 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。

二.信号相关函数

2.1 signal函数

#include 

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

函数作用:注册信号捕捉函数
函数参数
    signum:信号编号
    handler:信号处理函数

代码示例

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

/*signal函数测试---注册信号处理函数*/

void sighandler(int signo){
    printf("singo = %d\n",signo);
}

int main(){
    signal(SIGPIPE,sighandler);
    //没给读端的管道写数据产生SIGPIPE信号。
    int fd[2];
    int ret = pipe(fd);
    if(ret < 0){
        perror("pipe");
        return -1;
    }

    close(fd[0]);

    write(fd[1],"hello",strlen("hello")); 
    // 信号产生后,内核调用注册的sighandler
    return 0;
    
}

2.2 kill函数


#include 
#include 
​
int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死)
​
参数:
    pid : 取值有 4 种情况 :
        pid > 0:  将信号传送给进程 ID 为pid的进程。
        pid = 0 :  将信号传送给当前进程所在进程组中的所有进程。
        pid = -1 : 将信号传送给系统内所有的进程。
        pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
    sig : 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
​
返回值:
    成功:0
    失败:-1

代码示例

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

int main(){
    kill(getpid(),SIGKILL); // 给当前进程发送信号SIGKILL
    printf("helloworld\n");
    return 0;
}

2.3 raise函数

#include 
​
int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
    sig:信号编号
返回值:
    成功:0
    失败:非0值

代码示例

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

//信号处理函数
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    // 注册信号处理函数
    signal(SIGINT,sighandler);

    // 给当前进程发送SIGINT信号
    raise(SIGINT);
    return 0;

}

2.4 abort

#include 
​
void abort(void);
功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);
​
参数:无
​
返回值:无
​

代码示例

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

//信号处理函数
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    // 注册信号处理函数
    signal(SIGINT,sighandler);

    // 给当前进程发送SIGINT信号
    raise(SIGINT);
    // abort 给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);
    abort();
    return 0;

}

2.5 alarm 函数

#include 
​
unsigned int alarm(unsigned int seconds);
功能:
    设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
    取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
    seconds:指定的时间,以秒为单位
返回值:
    返回0或剩余的秒数

代码示例

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

int main(){
    int n = alarm(5);
    printf("n = %d\n",n);
    sleep(1);
    n = alarm(5);
    printf("n = %d\n",n);
    return 0;
}

2.6 setitimer函数

#include 
​
int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
功能:
    设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
    which:指定定时方式
        a) 自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
        b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM  只计算进程占用cpu的时间
        c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
    new_value:struct itimerval, 负责设定timeout时间
        struct itimerval {
            struct timerval it_interval; // 闹钟触发周期
            struct timerval it_value;    // 闹钟触发时间
        };
        struct timeval {
            long tv_sec;            // 秒
            long tv_usec;           // 微秒
        }
        itimerval.it_value: 设定第一次执行function所延迟的秒数 
        itimerval.it_interval:  设定以后每几秒执行function
​
    old_value: 存放旧的timeout值,一般指定为NULL
返回值:
    成功:0
    失败:-1

代码示例

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

//信号处理函数
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}

int main(){
    signal(SIGALRM,sighandler);

    struct itimerval tm;
    // 周期性时间赋值
    tm.it_interval.tv_sec = 1;
    tm.it_interval.tv_usec = 0;
    // 第一次出发的时间
    tm.it_value.tv_sec = 3;
    tm.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL,&tm,NULL);

    while (1)
    {
        sleep(1);
    }
    

    return 0;
}

三 信号集函数

3.1 信号集

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集。
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。
由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。

函数 函数说明 参数与返回值
int sigemptyset(sigset_t *set); 函数说明:将某个信号集清0 函数返回值:成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set); 函数说明:将某个信号集置1 函数返回值:成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum); 函数说明:将某个信号加入信号集合中 函数返回值:成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum); 函数说明:将某信号从信号清出信号集 函数返回值:成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum); 函数说明:判断某个信号是否在信号集中 函数返回值:在:1;不在:0;出错:-1,设置errno

代码示例

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


int main(){
    // 定义个信号集变量
    sigset_t set;
    int ret = 0;

    // 初始化信号集内容,也就是清空信号集
    sigemptyset(&set);

    // 判断SIGINT 是否在信号集set中,在返回1,不在返回0
    ret = sigismember(&set,SIGINT);
    if(ret == 0){
        printf("SIGINT not in set\n");
    }

    // 将SIGINT和SIGQUIT添加到信号集set
    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGQUIT);

    // 判断SIGINT 是否在信号集set中,在返回1,不在返回0
    ret = sigismember(&set,SIGINT);
    if(ret == 1){
        printf("SIGINT  in set\n");
    } 
    // 把 SIGQUIT 从信号集 set 移除
    sigdelset(&set, SIGQUIT); 

    // 判断SIGQUI 是否在信号集set中,在返回1,不在返回0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT not in set \n");
    }
    return 0;
}

3.2 sigprocmask函数

用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程控制块中的信号屏蔽字(阻塞信号集)。

#include 
​
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
​
参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
    set : 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
    oldset : 保存原先信号阻塞集地址
​
返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。

3.3 sigpending函数

#include 
​
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
    set:未决信号集
返回值:
    成功:0
    失败:-1

代码示例

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

//信号处理函数
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
}


int main(){
    //注册SIGINT和SIGQUIT的信号处理函数
    signal(SIGINT, sighandler);
    signal(SIGQUIT, sighandler);

    // 定义个信号集变量
    sigset_t myset,old;
    // 初始化信号集
    sigemptyset(&myset);

    // 添加信号集到阻塞中
    sigaddset(&myset, SIGINT);
    sigaddset(&myset, SIGQUIT);
    sigaddset(&myset, SIGKILL);

    // 自定义信号集设置到内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &myset, &old);

    sigset_t pend;
    int i = 0;
    while (1)
    {
        // 读取内核中的未决信号集
        sigpending(&pend);

        for(int i=1; i< 32;i++){
            if (sigismember(&pend, i))
            {
                printf("1");
            }
            else if (sigismember(&pend, i) == 0)
            {
                printf("0");
            }
            printf("\n");
        }
        printf("\n");
        sleep(5);
        i++;

        if(i<5){
             sigprocmask(SIG_SETMASK, &old, NULL);
        
        }


    }
    
    return 0;
}

四.信号捕捉

一个进程收到一个信号的时候,可以用如下方法进行处理:
1)执行系统默认动作:对大多数信号来说,系统默认动作是用来终止该进程。
2)忽略此信号(丢弃)
3)执行自定义信号处理函数(捕获)

SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

信号捕捉函数常用的有两个 signal函数和sigaction函数。
signal函数上述已有说明。

4.1 sigaction函数

#include 
​
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。
​
参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。
​
    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
​
返回值:
    成功:0
    失败:-1


struct sigaction {
       void  (*sa_handler)(int);    // 旧的信号处理函数
       void  (*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数
       sigset_t  sa_mask; //信号处理函数执行期间需要阻塞的信号
       int      sa_flags; //通常为0,表示使用默认标识
       void     (*sa_restorer)(void);
};

代码示例

#include 
#include 
#include 
#include 
#include 

//信号处理函数
void sighandler(int signo)
{
    printf("signo==[%d]\n", signo);
    sleep(4);
}

int main()
{
    //注册信号处理函数
    struct sigaction act;
    act.sa_handler = sighandler;
    sigemptyset(&act.sa_mask);  //在信号处理函数执行期间, 不阻塞任何信号
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, NULL);

    
    signal(SIGQUIT, sighandler);    

    while(1)
    {
        sleep(10);
    }

    return 0;
}

4.2 内核实现信号捕捉过程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
以如下例子说明
1.用户程序注册了SIGQUIT信号的处理函数sighandler。
2.当前正在执行main函数,这时发生中断或异常切换到内核态。
3.在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4.内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5.sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了

五.SIGCHLD信号

5.1 产生SIGCHLD信号的条件

1) 子进程终止时

2) 子进程接收到SIGSTOP信号停止时

3) 子进程处在停止态,接受到SIGCONT后唤醒时

5.2 SIGCHLD信号的作用

子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号后可以对子进程进行回收。
使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。

5.3 避免僵尸进程

父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
如果父进程要处理的事情很多,不能够挂起,可以通过 signal() 函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。
代码示例

//父进程使用SICCHLD信号完成对子进程的回收
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void waitchild(int signo)
{
    pid_t wpid;

    //回收子进程
    while(1)
    {
        wpid = waitpid(-1, NULL, WNOHANG);
        if(wpid>0)
        {
            printf("child is quit, wpid==[%d]\n", wpid);
        }
        else if(wpid==0)
        {
            printf("child is living, wpid==[%d]\n", wpid);
            break;
        }
        else if(wpid==-1)
        {
            printf("no child is living, wpid==[%d]\n", wpid);
            break;
        }
    }
}

int main()
{
    pid_t pid;

    signal(SIGCHLD,waitchild);

    pid = fork();
    if(pid < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){
        printf("child process [%d]\n",getpid());
        exit(0);
    }else{
        sleep(2);
        printf("father process [%d]\n",getpid());
        system("ps -ef | grep defunct"); // 检查有无僵尸进程
    }
    return 0;

    return 0;
}

5.4 父进程忽略子进程,子进程结束后,由内核回收

用signal(SIGCHLD, SIG_IGN)通知内核,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。

//父进程使用SICCHLD信号完成对子进程的回收
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t pid;

    signal(SIGCHLD,SIG_IGN);

    pid = fork();
    if(pid < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){
        printf("child process [%d]\n",getpid());
        exit(0);
    }else{
        sleep(2);
        printf("father process [%d]\n",getpid());
        system("ps -ef | grep defunct");
    }
    return 0;

    return 0;
}

你可能感兴趣的