九、 信号

1.基本概念

1)什么是信号?

信号是提供异步事件处理机制的软件中断
这些异步事件可能来自硬件设备,也可能来自系统内核,甚至可能来自用户程序。进程之间可以相互发送信号,这使信号成为一种进程间通信(Inter-Process Communication, IPC)的基本手段。信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的。程序设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切尽在内核操控下异步地发生。

例如:

  • 输入命令,在Shell下启动一个前台进程。
  • 用户按下Ctrl-C,键盘输入产生一个硬件中断。
  • 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行, CPU从用户 态切换到内核态处理硬件中断。
  • 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一 个SIGINT信号给该进程)。
  • 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接
    终止进程而不再返回它的用户空间代码执行。

2)什么是信号处理?

每一个信号都有其生命周期:

产生:信号被生成,并被发送至系统内核
未决:信号被内核缓存,而后被递送至目标进程
递送:内核已将信号发送至目标进程
   忽略 - 什么也不做。
   捕获 - 暂定当前的执行过程,转而调用一个事先写好的信号处理函>>数,待该函数       完成并返回后,再继续之前被中断的过程。
   默认 - 既不忽略该信号,也不用自己定义处理方式,而是按照系统>>默认的方式予以       响应。

激励(信号)->响应(信号处理)

3)信号的名称和编号

信号名称:形如SIGXXX的字符串或宏定义,提高可读性。
信号编号:整数

通过kill -l命令查看当前系统所支持的全部信号名称及其编号。
1~31:31个不可靠信号,也叫非实时信号。
34~64:31个可靠信号,也叫实时信号。
共62个信号,注意没有32和33信号。

SIGHUP(1),控制终端关闭,终止
SIGINT(2),用户产生中断符(Ctrl+C),终止
SIGQUIT(3),用户产生退出符(Ctrl+),终止+转储
SIGBUS(7),硬件或内存对齐错误,终止+转储
SIGKILL(9),不能被捕获和忽略,终止
SIGSEGV(11),无效内存访问,终止+转储
SIGPIPE(13),向读端已关闭的管道写入,终止
SIGALRM(14),alarm函数设置的闹钟到期,终止
SIGTERM(15),可被捕获和忽略,终止
SIGCHLD(17),子进程终止,忽略
SIGIO(29),异步I/O事件,终止

可在终端对指定进程发送信号,例如kill -2 <pid号> 或 kill -SIGINT <pid号>

2.捕获信号

signal函数(捕获处理)

#include <signal.h>
typedef void (*sighandler_t) (int);
sighandler_t:函数指针, 指向一个接受整型参数且无返回值的函数
设置针对特定信号的处理方式,即捕获特定的信号:
sighandler_t signal(int signum, sighandler_t handler);

成功返回原信号处理方式,失败返回SIG_ERR(sighandler_t类型的-1)。
signum - 信号编号
handler - 信号处理函数指针,也可以取以下值:
    SIG_IGN - 忽略信号
    SIG_DFL - 默认操作

// 定义信号处理函数
void sigint (int signum) {
SIGINT(2)信号的处理代码
}

// 捕获SIGINT(2)信号
if (signal(SIGINT, sigint) == SIG_ERR) {
perror(“signal”);
ret rn -1;
}

signal(SIGINT, SIG_DFL); // 按默认方式处理
signal(SIGINT, SIG_IGN); // 忽略信号

当一个信号正在被处理的过程中,相同的信号再次产生,该信号会被阻塞,直到前一个信号处理完成,即从信号处理函数中返回,后一个被阻塞的信号才被递送,进而再次执行信号处理函数。当一个不可靠信号正在被处理的过程中,多个相同的信号再次产生,只有第一个信号会被阻塞,其它信号直接丢弃,如果是可靠信号,都会被阻塞,并按照产生的顺序依次被递送。

信号处理函数及被其调用的函数都有可能发生重入,由此可能引发无可预知的风险。

所有标准I/O函数都是不可重入函数,因此在信号处理的过程中要慎用。(以及慎用全局变量、静态变量、堆区数据等)
A信号->A信号处理函数 \ 打印AAAAAA> printf(调试信息);
B信号->B信号处理函数 / 打印BBBBBB> printf(调试信息);
结果可能:AAABBBBBBAAA

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigint(int signum) 
{
    signal(SIGINT, SIG_DFL);
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
    signal(SIGINT, sigint);
}
void sigterm(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
    printf("妥当善后...\n");
    exit(0);
}
int main(void) 
{
    if (signal(/*2*/SIGINT, sigint) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(/*3*/SIGQUIT, SIG_IGN) ==
        SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(/*9*//*SIGKILL*//*15*/SIGTERM,      //SIGKILL不能被捕获也不能被忽略
        /*SIG_IGN*/sigterm/*SIG_DFL*/)==SIG_ERR){  //SIGTERM可以
        perror("signal");
        return -1;
    }
    for (;;);
    return 0;
}

3.信号捕获流程

在这里插入图片描述

信号的本质是一个中断的处理过程,而非多线程的并发过程。

线程安全的函数未必是可重入函数。
线程安全依靠的是锁机制,可重入函数靠局部化。

4.信号捕获的一次性问题

在某些非Linux操作系统上,存在信号捕获的一次性问题:
即使设置了对某个信号的捕获,只有设置后的第一个该信号被递送时,信号处理函数会被执行,以后再来相同的信号,均按默认方式处理。如果希望对信号的捕获具有持久性,可以在信号处理函数返回前再次设置对该信号的捕获。

5.太平间信号

通过SIGCHLD(17)信号高效地回收子进程僵尸。
高效:及时性,适时性。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sigchld(int signum) 
{
    for (;;) { //非实时信号只能阻塞一个信号,其它的会被丢弃,防止信号被丢弃时无法处理由此产生的僵尸进程
        pid_t pid = waitpid(-1, NULL, WNOHANG);//非阻塞,防止浪费资源
        if (pid == -1) {
            if (errno != ECHILD) {
                perror("wait");
                exit(-1);
            }
            printf("父进程:所有子进程"
                "都已终止。\n");
            break;
        }
        if (!pid) {
            printf("父进程:暂时没有可回收的"
                "子进程。\n");
            break;
        }
        printf("父进程:%d子进程终止。\n", pid);
    }
    usleep(100000);//使得当前信号没有被处理完,又有信号被捕获
    			   //(非实时信号只能阻塞一个信号,其它的会被丢弃,所以这里会产生3个僵尸进程)
}
int main(void) 
{
    if (signal(SIGCHLD, sigchld) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    for (int i = 0; i < 5; ++i) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            return -1;
        }
        if (pid == 0) {
            printf("子进程:我是%d进程。"
                "我要退出了。\n", getpid());
            return 0;
        }
    }
    getchar();
    return 0;
}

6.信号处理的继承与恢复

①fork/vfork函数创建的子进程会继承父进程的信号处理方式,直到子进程调用exec函数创建新进程替代其自身为止。
② exec函数创建的新进程会将原进程中被设置为捕获的信号还原为默认处理。在原进程中被忽略的信号于新进程中继续被忽略。

//继承信号
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
}
int main(void) 
{
    if (signal(SIGINT, sigint) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(SIGQUIT, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        printf("%d进程:我是子进程,"
            "正在运行中...\n", getpid());
        for (;;);
        return 0;
    }
    sleep(1);
    printf("%d进程:我是父进程,马上要退出。\n",
        getpid());
    return 0;
}
//恢复默认信号
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
}
int main(void) 
{
    if (signal(SIGINT, sigint) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(SIGQUIT, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    pid_t pid = vfork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        printf("%d进程:我是子进程,"
            "创建loop进程...\n", getpid());
        if (execl("./loop", "loop", NULL) == -1){
            perror("execl");
            _exit(-1);
        }
        _exit(0);
    }
    sleep(1);
    printf("%d进程:我是父进程,马上要退出。\n",
        getpid());
    return 0;
}

7.发送信号

1)通过键盘向当前拥有控制终端的前台进程发送信号

  • Ctrl+C -> SIGINT(2) 默认终止进程
  • Ctrl+\ -> SIGQUIT(3),默认终止进程且转储
  • Ctrl+Z ->SIGTSTP(20),默认停止(挂起)进程

2)来自硬件或者内核的错误和异常引发的信号

  • SIGILL(4),进程试图执行非法指令
  • SIGBUS(7),硬件或总线对齐错误
  • SIGFPE(8),浮点异常
  • SIGSEGV(11),无效内存访问
  • SIGPIPE(13),向无读端的管道写入
  • SIGSTKFLT(16),浮点数协处理器栈错误
  • SIGXFSZ(25),文件资源超限
  • SIGPWR(30),断电
  • SIGSYS(31),无效系统调用

3)通过kill命令发送信号

kill [-信号] PIDs
缺省发送SIGTERM(15)信号。
超级用户可以给任何进程发信号,普通用户只能给自己的进程发信号。

4)调用函数发送信号

kill函数(发送)

向特定的进程(组)发送信号:
int kill(pid_t pid, int signum);
成功(至少发出去一个信号)返回0,失败返回-1。

pid - 进程(组)标识,可取以下值:
           <-1:向-pid进程组中的所有进程发送信号
           -1: 向系统中的所有进程发送信号
            0: 向调用进程同组的所有进程发送信号
            >0: 向进程标识为pid的特定进程发送信号
signum - 信号编号,取0用于检查pid进程是否存在,如果不存在,kill函数会返回-1,且置errno为ESRCH。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
    exit(0);
}
int isdead(pid_t pid) 
{
    if (kill(pid, 0) == -1) {
        if (errno != ESRCH) {
            perror("kill");
            exit(-1);
        }
        return 1;
    }
    return 0;
}
int main(void) 
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        printf("%d进程:我是子进程,"
            "正在运行中...\n", getpid());
        if (signal(SIGINT, sigint) == SIG_ERR) {
            perror("signal");
            return -1;
        }
        for (;;);
        return 0;
    }
    sleep(1);
    printf("%d进程:我是父进程,我要发信号...\n",
        getpid());
    if (kill(pid, SIGINT) == -1) {
        perror("kill");
        return -1;
    }
    printf("%d进程:%d进程%s终止。\n",
        getpid(), pid,
        isdead(pid) ? "已然" : "尚未");
    if ((pid = wait(NULL)) == -1) {
        perror("wait");
        return -1;
    }
    printf("%d进程:%d进程%s终止。\n",
        getpid(), pid,
        isdead(pid) ? "已然" : "尚未");
    return 0;
}

raise函数 (发送)

向调用进程自己发送信号:
int raise(int signum); 成功返回0,失败返回-1。
signum - 信号编号

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
    usleep(100000);
    /*
    exit(0);
    */
}
int main(void) 
{
    if (signal(SIGINT, sigint) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    printf("%d进程:我要给自己发信号...\n",
        getpid());
    for (int i = 0; i < 5; ++i)
        /*
        if (raise(SIGINT) == -1) {
            perror("raise");
            return -1;
        }
        */
        if (kill(getpid(), SIGINT) == -1) {
            perror("kill");
            return -1;
        }
    /*
    for (;;)
        printf("%d进程:我还活着...\n",
            getpid());
    */
    for (;;);
    return 0;
}

通过raise或kill向调用进程发送信号,如果该信号被捕获,则要等到信号处理函数返回后,这两个函数才会返回。 所以不存在多个不可靠信号被丢弃的情况。

8.暂停、睡眠和闹钟

pause函数

暂停,即不受时间限制的睡眠:
int pause(void);
成功阻塞,失败返回-1。

该函数使调用进程进入无时限的睡眠状态,即不参与内核调度,直到有信号终止了调用进程或被捕获。如果有信号被调用进程捕获,当信号处理函数返回以后,pause函数才会返回,且返回值为-1,同时置errno为EINTR,表示阻塞的系统调用被信号中断。pause函数要么不返回,要么返回-1(捕获了返回),不会返回0(终止了不返回)。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
}
int main(void) 
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        if (signal(SIGINT, sigint) == SIG_ERR) {
            perror("signal");
            return -1;
        }
        printf("%d进程:我是子进程,"
            "大睡无期...\n", getpid());
        pause();
        printf("%d进程:我醒了,即将退出。\n",
            getpid());
        return 0;
    }
    sleep(10);
    printf("%d进程:我是父进程,"
        "向子进程发送信号...\n", getpid());
    if (kill(pid, SIGINT) == -1) {
        perror("kill");
        return -1;
    }
    if ((pid = wait(NULL)) == -1) {
        perror("wait");
        return -1;
    }
    printf("%d进程:子进程已终止。\n", getpid());
    return 0;
}

sleep函数

受时间限制的睡眠:
unsigned int sleep(unsigned int seconds);
返回0或剩余秒数。
seconds - 以秒为单位的睡眠时限
该函数使调用进程睡眠seconds秒,除非有信号终止了调用进程或被其捕获。如果有信号被调用进程捕获,在信号函数返回以后,sleep函数才会返回,且返回值为剩余秒数,否则该函数返回0,表示睡眠充足。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void sigint(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
}
int main(void) 
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        if (signal(SIGINT, sigint) == SIG_ERR) {
            perror("signal");
            return -1;
        }
        printf("%d进程:我是子进程,"
            "睡眠10秒...\n", getpid());
        unsigned int left = sleep(10);
        printf("%d进程:我醒了,"
            "还剩%u秒没睡,即将退出。\n",
            getpid(), left);
        return 0;
    }
    sleep(15);
    printf("%d进程:我是父进程,"
        "向子进程发送信号...\n", getpid());
    if (kill(pid, SIGINT) == -1) {
        perror("kill");
        return -1;
    }
    if ((pid = wait(NULL)) == -1) {
        perror("wait");
        return -1;
    }
    printf("%d进程:子进程已终止。\n", getpid());
    return 0;
}

usleep函数

int usleep(useconds_t usec);
睡够了返回0,睡不够返回-1,同时置errno为EINTR。
usec - 为微秒为单位的睡眠时限
     微秒=10^-6秒 Intel
 IntelCPU:50~55毫秒的时钟周期,所以尽量不要小于采用CPU的时间周期区间

alarm函数

设置闹钟
unsigned int alarm(unsigned int seconds);
返回0或先前闹钟的剩余时间。
seconds - 以秒为单位的闹钟时间
alarm函数使系统内核在该函数被调用以后seconds秒的时候,向调用进程发送SIGALRM(14)信号。若在调用该函数前已设过闹钟且尚未到期,则该函数会重设闹钟,并返回先前所设闹钟的剩余秒数,否则返回0
若seconds参数取0,则取消之前设置过且未到期的闹钟。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigalrm(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
    exit(0);
}
int main(void) 
{
    if (signal(SIGALRM, sigalrm) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    alarm(10);
    printf("定时10秒。\n");
    printf("睡眠3秒...\n");
    sleep(3);
    /*
    unsigned int remain = alarm(5);
    printf("定时5秒,剩余%u秒。\n", remain);
    */
    unsigned int remain = alarm(0);
    printf("取消定时,剩余%u秒。\n", remain);
    for (;;);
    return 0;
}

通过alarm函数所设置的定时只是一次性的,即在定时到期时发行一次SIGALRM(14)信号,此后不会再发送该信号。如果希望获得周期性的定时效果,可以在SIGALRM(14)信号的处理函数中继续调用alarm函数,完成下一个定时的设置。

#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
void sigalrm(int signum) 
{
    time_t t = time(NULL);
    struct tm* lt = localtime(&t);//获取本地时间
    printf("\r%02d:%02d:%02d", lt->tm_hour,
        lt->tm_min, lt->tm_sec);
    alarm(1);
}
int main(void) 
{
    setbuf(stdout, NULL);
    if (signal(SIGALRM, sigalrm) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    sigalrm(SIGALRM);
    for (;;);
    return 0;
}

9.信号集

#include <signal.h>
typedef __sigset_t sigset_t;

#include <sigset.h>
#define  _SIGSET_NWORDS   (1024 / (8 * sizeof(unsigned long int)))

#include <sigset.h>
typedef struct {
   unsigned long int __val[_SIGSET_NWORDS];
       对于32位系统可计算得_SIGSET_NWORDS为32
       这里其实是按特定的位来存储信号的,所以这里有4 * 8 * 32 = 1024位
} __sigset_t;

sigfillset 函数

填满信号集,即将信号集的全部信号位置1
int sigfillset(sigset_t* sigset);

sigemptyset 函数

清空信号集,即将信号集的全部信号位置0
int sigemptyset(sigset_t* sigset);

sigaddset 函数

加入信号,即将信号集中的特定信号位置1
int sigaddset(sigset_t* sigset, int signum);

sigdelset 函数

删除信号,即将信号集中的特定信号位置0
int sigdelset(sigset_t* sigset, int signum);
成功返回0,失败返回-1。

sigismember 函数

检查信号,即判断信号集中的特定信号位是否为1
int sigismember(sigset_t* sigset, int signum);
有则返回1,无则返回0,失败返回-1。

#include <stdio.h>
#include <signal.h>
void printb(char byte) //二进制打印
{
    for (int i = 0; i < 8; ++i)
        printf("%c",
            (byte & (1 << (7 - i))) ? '1' : '0');
    printf(" ");
}
void printm(void* buff, size_t size) //使用二进制打印一块内存的数据
{
    for (int i = 0; i < size; ++i) {
        printb(((char*)buff)[size-i-1]);
        if ((i + 1) % 4 == 0 || i == size - 1)
            printf("\n");
    }
}
int main(void)
{
    sigset_t sigset;
    printf("满信号集:\n");
    sigfillset(&sigset);
    printm(&sigset, sizeof(sigset));
    printf("空信号集:\n");
    sigemptyset(&sigset);
    printm(&sigset, sizeof(sigset));
    printf("加入SIGINT(2)信号:\n");
    sigaddset(&sigset, SIGINT);
    printm(&sigset, sizeof(sigset));
    printf("加入SIGQUIT(3)信号:\n");
    sigaddset(&sigset, SIGQUIT);
    printm(&sigset, sizeof(sigset));
    printf("删除SIGQUIT(3)信号:\n");
    sigdelset(&sigset, SIGQUIT);
    printm(&sigset, sizeof(sigset));
    printf("%d\n", sigismember(&sigset,SIGINT));
    printf("%d\n", sigismember(&sigset,SIGQUIT));
    return 0;
}

10.信号屏蔽

1)递送、未决和掩码

当信号产生时,系统内核在目标进程的进程表项中,以信号位置1的方式,存储该信号,这个过程就叫做递送

信号从产生到完成递送之间存在一定的时间间隔,处于该间隔状态的信号就是属于未决信号

每个进程都有一个信号掩码,它实际上是一个信号集,位于该信号集中的信号一旦产生,并不会被递送给相应的进程,而是会被阻塞于未决状态。
当进程正在执行类似更新数据库、设置硬件状态等敏感任务时,可能不希望被某些信号中断。这是可以通过信号掩码暂时屏蔽而非忽略这些信号,使其一旦产生即被阻塞于未决状态,待特定任务完成以后,在恢复对这些信号的处理。

不可靠信号最多被信号掩码屏蔽一次,在屏蔽期间再有更多的相同信号,一律被丢弃。
可靠信号会全部被保留下来,且按照发送的顺序排成队列。

2)设置信号掩码

sigprocmask函数

int sigprocmask(int how, const sigset_t* sigset ,sigset_t* oldset);
成功返回0,失败返回-1。
how - 信号掩码的修改方式,可取以下值:
        SIG_BLOCK -将sigset中的信号加入当前掩码
        SIG_UNBLOCK -从当前掩码中删除sigset中的信号
        SIG_SETMASK -将sigset设置为当前掩码
sigset - 信号集,取NULL则忽略参数
oldset - 原信号掩码,取NULL则忽略此参数

3)获取未决信号

sigpending函数

int sigpending(sigset_t* sigset);
成功返回0,失败返回-1。
sigset - 输出未决信号集

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sighand(int signum) 
{
    printf("%d进程:收到%d信号!\n",
        getpid(), signum);
}
void update(void) //更新数据任务,不希望被信号打扰
{
    for (int i = 0; i < 3; ++i) {
        printf("%d进程:更新第%d条记录。\n",
            getpid(), i + 1);
        sleep(1);
    }
}
int main(void) 
{
    if (signal(SIGINT, sighand) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(50, sighand) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    printf("%d进程:屏蔽%d信号和%d信号。\n",
        getpid(), SIGINT, 50);
    sigset_t newset;
    sigemptyset(&newset);
    sigaddset(&newset, SIGINT);
    sigaddset(&newset, 50);
    sigset_t oldset;
    if (sigprocmask(SIG_SETMASK, &newset,  //设置对信号2和50的掩码
        &oldset) == -1) {
        perror("sigprocmask");
        return -1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        pid_t ppid = getppid();
        for (int i = 0; i < 3; ++i) {
            printf("%d进程:向%d进程发送"
                "%d信号和%d信号...\n", getpid(),
                ppid, SIGINT, 50);
            kill(ppid, SIGINT);
            kill(ppid, 50);
        }
        return 0;
    }
    update();
    /*
    if (sigprocmask(SIG_SETMASK, &oldset,//恢复为原来的信号掩码,使得成为未决信号的2和50可以被处理
        NULL) == -1) {
        perror("sigprocmask");
        return -1;
    }
    */
    sigset_t pendingset;
    if (sigpending(&pendingset) == -1) { //获取未决的信号,在pendingset结构体中
        perror("sigpending");
        return -1;
    }
    if (sigismember(&pendingset, SIGINT)) //通过sigismember函数查看屏蔽的信号集中有无信号2和50这两个未决信号
    					  				  //有的话进行处理
        printf("处理%d信号...\n", SIGINT);
    if (sigismember(&pendingset, 50))
        printf("处理%d信号...\n", 50);
    getchar();
    return 0;
}

11.现代版本的信号处理与发送

经典版本的信号处理与发送:signal/kill(raise)

现代版本的信号处理与发送:sigaction/sigqueue

sigaction函数 (捕获处理)

int sigaction(int signum, const struct sigaction* sigact,struct sigaction* oldact);
成功返回0,失败返回-1。
signum - 信号编号
sigact - 信号行为
oldact - 输出原来的信号行为,可置NULL

当signum信号被递送时,按sigact结构所描述的行为响应之。若oldact参数非NULL,则通过该参数输出原来的响应行为。
struct sigaction {
   // 经典版本的信号处理函数指针
   void (* sa_handler)(int);
   // 现代版本的信号处理函数指针
   void (* sa_sigaction)(int, siginfo_t*, void*);
   // 信号处理期间的附加掩码集
   sigset_t sa_mask;
   // 信号处理标志
   int sa_flags;
   // 预留项,目前置NULL
   void (* sa_restorer)(void);
};

sigqueue函数(发送)

int sigqueue(pid_t pid, int signum, const union sigval value);
成功返回0,失败返回-1。
pid - 接收信号进程的PID
signum - 信号编号
value - 信号附加数据 ( 由siginfo_t结构体中的si_value提供)

现代版本的信号处理函数:
void <处理函数名>(int signum, siginfo_t* si, void* reserved)
signum - 信号编号
si - 信号信息
reserved - 保留未用

typedef struct siginfo {
   // 发送信号进程的PID
   pid_t si_pid;
   // 信号附加数据
   sigval_t si_value;
   …
} siginfo_t;

typedef union sigval {
   // 用整型作为信号附加数据
   int sival_int;
   // 用任意类型的指针作为信号附加数据
   void* sival_ptr;
   …
} sigval_t;

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void siginfo(int signum, siginfo_t* si,
    void* pv) 
    {
    printf("%d进程:收到来自%d进程的%d信号"
        "(附加值=%d)!\n", getpid(), si->si_pid,
        signum, si->si_value.sival_int);
    usleep(100);
}
int main(void) 
{
    int signum = /*50*/SIGINT;
    struct sigaction sa = {};
    sa.sa_sigaction = siginfo;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    if (sigaction(signum, &sa, NULL) == -1) {
        perror("sigaction");
        return -1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        pid_t ppid = getppid();
        for (int i = 0; i < 10; ++i) {
            sigval_t sv;
            sv.sival_int = i;
            printf("%d进程:向%d进程发送%d信号"
                "(附加值=%d)...\n", getpid(),
                ppid, signum, sv.sival_int);
            if (sigqueue(ppid, signum,
                sv) == -1) {
                perror("sigqueue");
                return -1;
            }
        }
        printf("%d进程:我即将退出。\n",
            getpid());
        return 0;
    }
    if ((pid = wait(NULL)) == -1) {
        perror("wait");
        return -1;
    }
    printf("%d进程:%d进程已退出。\n",
        getpid(), pid);
    return 0;
}

1)增减信号掩码

  • 缺省情况下,在信号处理函数的执行过程中,会自动屏蔽这个正在被处理的信号,而对于其它信号则不会屏蔽。
  • 通过sa_mask字段可以人为指定,在信号处理函数执行期间,除正在被处理的这个信号以外,还想屏蔽哪些信号,并在信号处理函数返回后,自动解除对它们的屏蔽。
  • 另一方面,还可以通过为sa_flags字段设置SA_NOMASK/SA_NODEFER标志位,告诉系统内核在信号处理函数执行期间,不要屏蔽这个正在被处理的信号。

2)选择信号处理函数的风格

如果sa_flags字段中没有SA_SIGINFO标志位,则sa_handler字段有效,即使用经典版本的信号处理函数。
相反,如果sa_flags字段包含SA_SIGINFO标志位,则sa_sigaction字段有效,即使用现代版本的信号处理函数。

3)一次性信号处理

如果sa_flags字段中包含SA_ONESHOT/SA_RESETHAND标志位,那么对所捕获信号的处理就是一次性的,即在执行完一次信号处理函数后,即恢复为按默认方式处理。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigint1(int signum) 
{
    printf("收到%d信号!睡眠中...\n", signum);
    sleep(5);
    printf("睡醒了。\n");
}
void sigint2(int signum, siginfo_t* si,void* pv) 
{
    printf("收到发自%d进程的%d信号!\n",
        si->si_pid, signum);
}
int main(void) 
{
    struct sigaction sa = {};
    /*
    printf("使用经典版本信号处理函数执行期间只屏蔽"
        "SIGINT信号,不屏蔽SIGQUIT信号。\n");
    sa.sa_handler = sigint1;
    *//*
    printf("使用经典版本信号处理函数执行期间既屏蔽"
        "SIGINT信号,也屏蔽SIGQUIT信号。\n");
    sa.sa_handler = sigint1;
    sigaddset(&sa.sa_mask, SIGQUIT);
    *//*
    printf("使用经典版本信号处理函数执行期间不屏蔽"
        "SIGINT信号,也不屏蔽SIGQUIT信号。\n");
    sa.sa_handler = sigint1;
    sa.sa_flags = SA_NOMASK;  //使得正在被捕获的信号不被屏蔽
    *//*
    printf("使用经典版本信号处理函数执行期间不屏蔽"
        "SIGINT信号,只屏蔽SIGQUIT信号。\n");
    sa.sa_handler = sigint1;
    sigaddset(&sa.sa_mask, SIGQUIT);//屏蔽SIGQUIT信号
    sa.sa_flags = SA_NOMASK;//使得正在被捕获的信号不被屏蔽
    *//*
    printf("使用现代版本信号处理函数。\n");
    sa.sa_sigaction = sigint2;
    sa.sa_flags = SA_SIGINFO;//有该标志位则调用现代信号处理函数
    */
    printf("一次性信号处理。\n");
    sa.sa_handler = sigint1;
    sa.sa_flags = SA_ONESHOT;//此标志下,信号被处理一次后便恢复为默认
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return -1;
    }
    for (;;) pause();
    return 0;
}

4)系统调用中断重启

诸如pause、sleep、usleep等系统调用的阻塞过程会被信号打断,即在针对某个信号的处理函数返回后,这些系统调用也会从阻塞中返回。如果不希望这类系统调用被所捕获的信号打断,即在针对该信号的处理函数返回后,能够自动重启阻塞过程,而不要返回,可以为sa_flags字段添加SA_RESTART标志位。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void sigint(int signum) 
{
    printf("收到%d信号!\n", signum);
}
int main(void) 
{
    struct sigaction sa = {};
    sa.sa_handler = sigint;
    sa.sa_flags = SA_RESTART;//不会因信号而打断一些阻塞过程
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return -1;
    }
    ssize_t len;
    char buf[256] = {};
//again:
    if ((len = read(STDIN_FILENO, buf,
        sizeof(buf))) == -1) {
        //if (errno == EINTR) 若由系统中断的则再次读一遍
            //goto again;
        perror("read");
        return -1;
    }
    if (write(STDOUT_FILENO, buf, len) == -1) {
        perror("write");
        return -1;
    }
    return 0;
}

利用信号附加数据实现简单的进程间通信

//send.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char* argv[]) 
{
    if (argc < 2) {
        fprintf(stderr, "用法:%s <PID>\n",
            argv[0]);
        return -1;
    }
    char buf[1024];
    for (;;) {
        printf("> ");
        fgets(buf, sizeof(buf) / sizeof(
            buf[0]), stdin);
        if (!strcmp(buf, "!\n"))
            break;
        /*
        sigval_t sv = {};
        sv.sival_ptr = buf;
        sigqueue(atoi(argv[1]), 50, sv);
        */
        for (int i = 0; buf[i]; ++i) {
            sigval_t sv = {};
            sv.sival_int = buf[i];
            if (sigqueue(atoi(argv[1]),
                /*50*/SIGINT, sv) == -1) {//这里采用不可靠信号会导致信号被丢弃,使得附加值不能完全接收
                perror("sigqueue");
                return -1;
            }
            usleep(10000);//进行延迟,使得不可靠信号不会因阻塞而被丢弃
        }
    }
    return 0;
}
//receive.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigact(int signum, siginfo_t* si, void* pv)
{
    printf("%c", si->si_value.sival_int);
}
int main(void) 
{
    printf("PID: %d\n", getpid());
    setbuf(stdout, NULL);
    struct sigaction sa = {};
    sa.sa_sigaction = sigact;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    if (sigaction(/*50*/SIGINT, &sa, //这里采用不可靠信号会导致信号被丢弃,使得附加值不能完全接收
        NULL) == -1) {
        perror("sigaction");
        return -1;
    }
    getchar();
    return 0;
}

12.定时器

执行时间=用户时间+内核时间+睡眠时间

执行时间:直观感受、墙钟时间(真实计时器)
用户时间:耗在用户态的时间(虚拟计时器)
内核时间:消耗在内核态的时间
睡眠时间:消耗在等待I/O、睡眠等不被调度的时间

用户时间 + 内核时间 ----> 实用计时器
故:
内核时间 = 实用计时器 - 虚拟计时器
睡眠时间 = 真实计时器 - 实用计时器

利用真实计时器定时,到期信号:SIGALRM(14)
利用虚拟计时器定时,到期信号:SIGVTALRM(26)
利用实用计时器定时,到期信号:SIGPROF(27)

基于每种计时器的定时都有两个参数:
初始间隔 - 从开始定时到发出第一个到期信号的时间间隔。
重复间隔 - 每相邻两个到期信号的时间间隔。

setitimer函数

设置、开启和关闭定时器:
int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);
成功返回0,失败返回-1。
which - 指定设置哪个定时器,可取以下值:
      ITIMER_REAL: 基于真实计时器的定时
      ITIMER_VIRTUAL: 基于虚拟计时器的定时
      ITIMER_PROF: 基于实用计时器的定时
new_value - 新设置值
old_value - 输出原设置值,可置NULL

struct itimerval {
   // 重复间隔,取0表示只发一个信号,不重复,没有周期性
   struct timeval  it_interval;
   // 初始间隔,取0表示停止定时器,不再发信号
   struct timeval  it_value;
};

struct timeval {
   long  tv_sec; // 秒
   long  tv_usec; // 微秒
};

例如:5秒以后开始发SIGALRM(14)信号,以后每隔3毫秒再发一次该信号。

struct itimerval it;
it.it_value.tv_sec = 5; 
it.it_value.tv_usec = 0; 
it.it_interval.tv_sec = 0; 
it.it_interval.tv_usec = 3000; 
setitimer(ITIMER_REAL, &it, NULL); // 开
...
it.it_value.tv_sec = 0; //全置0时,定时器就是关闭状态
setitimer(ITIMER_REAL, &it, NULL); // 关
//如果希望立即启动定时器,初始间隔至少是1微秒,因为全置0时,定时器就是关闭状态。

PS:不能同时开启多个定时器

getitimer函数

获取当前定时器设置
int getitimer(int which, struct itimerval* curr_value);
成功返回0,失败返回-1。
which - 指定设置哪个定时器,可取以下值:
   ITIMER_REAL:基于真实计时器的定时
   ITIMER_VIRTUAL:基于虚拟计时器的定时
   ITIMER_PROF:基于实用计时器的定时
curr_value - 当前设置值

//计时器
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
int h = 0, m = 0, s = 0, ms = 0;//时、分、秒、毫秒
void sigalrm(int signum) 
{
    printf("\r%02d:%02d:%02d.%03d", h, m, s, ms);
    if (++ms == 1000) {
        ms = 0;
        if (++s == 60) {
            s = 0;
            if (++m == 60) {
                m = 0;
                ++h;
            }
        }
    }
}
void sigint(int signum) 
{
    static int run = 1;
    struct itimerval it = {};
    if (!run) {
        printf("\n");
        h = m = s = ms = 0;
        it.it_value.tv_usec = 1;
        it.it_interval.tv_usec = 1000;
    }
    setitimer(ITIMER_REAL, &it, NULL);
    run ^= 1;
}
int main(void) 
{
    setbuf(stdout, NULL);
    if (signal(SIGALRM, sigalrm) == SIG_ERR) {
        perror("signal");
        return -1;
    }
    if (signal(SIGINT, sigint) == SIG_ERR) {
        perror("sigint");
        return -1;
    }
    struct itimerval it = {{0, 1000}, {5, 0}};
    if (setitimer(ITIMER_REAL, &it, NULL) == -1){
        perror("setitimer");
        return -1;
    }
    if (getitimer(ITIMER_REAL, &it) == -1) {
        perror("getitimer");
        return -1;
    }
    printf("初始间隔:%ld秒%ld微秒\n",
        it.it_value.tv_sec,
        it.it_value.tv_usec);
    printf("重复间隔:%ld秒%ld微秒\n",
        it.it_interval.tv_sec,
        it.it_interval.tv_usec);
    for (;;) pause();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值