9.进程信号

信号量

  1. 信号量是什么?

​ 本质是一个计数器,通常用来表示公共资源中,资源数量多少的问题。

​ 公共资源:可以被多个进程同时访问的资源。

  • 访问没有保护的公共资源会导致数据不一致问题

    1. 什么是数据不一致问题

​ 由于公共资源没有被保护,那么当写端还没有写完时,读端就可能已经进行读取了(比如说,写端想要将"qwy"都写入之后,再让读端进行读取,但是因为公共资源并没有被保护,可能写端仅仅输入一个"q",此时就被读端读取了)

复制代码
3. 为什么要让不同的进程看到同一份资源呢?
  • 因为我们想要通信,通过进程之间的通信,让进程之间实现协同。

  • 因为进程具有独立性,因此想要实现进程间的通信,就必须让多个进程看到同一份资源(这份公共资源又会导致数据不一致问题

  • 我们将被保护起来的公共资源称为临界资源,有大部分的资源是独立的(比如其中一个进程独自申请的堆空间和栈空间资源,这部分资源不属于公共资源,是独属于申请的这个进程的资源)

  1. 公共资源(内存,文件,网络等)是要被使用的,如何被进程使用呢?
  • 一定是该进程有对应的代码来访问这部分临界资源(被保护起来的公共资源),而这部分对应的代码被称为临界区。该进程其余的代码被称为非临界区。
  1. 如何来保护公共资源?
  • 采用互斥和同步的方式

  • 互斥:当写端在进行写入时,读端不能够进行读取;当读端进行读取时,写端不能够对其进行写入

  1. 原子性
  • 要么不做,要么做完,只有这两种状态的情况情况称为原子性
  • 例子:比如生活中,我们写课后作业,要么就不去做,一旦开始做,那就一口气将所有的作业都完成。

为什么要信号量

  1. 先举一个简单的例子
  • 当我们去电影院去看电影时,我们是不是坐在对应的座位上,这个座位就属于我们了呢?
  • 显然,并不是
  • 我们想要获取某个座位,那么我们首先需要做的事情就是去买电影票,电影票上面会标注有对应的座位号(座位号对应的座位就是我们预定的座位),以及票号(就是标注当前这张电影票是卖出去的第几张票)。
  • 因此,我们可以得知看电影买票的本质就是对放映厅中的座位进行预订。
  1. 信息量就相当于是电影票
  • 共享资源:a.将共享资源作为一个整体使用; b.将整块共享资源划分为一个一个的资源子部分
c 复制代码
// 信号量的相关接口
semget()
semctl()
semop()

IPC资源的组织方式

  1. 共享资源
  • 接口:shmctl()
c 复制代码
The buf argument is a pointer to a shmid_ds structure, defined in <sys/shm.h> as follows:
struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Last change time */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };

       The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):

           struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
           };
  1. 消息队列
  • 接口:msgctl()
c 复制代码
The msqid_ds data structure is defined in <sys/msg.h> as follows:

           struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };

       The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):

           struct ipc_perm {
               key_t          __key;       /* Key supplied to msgget(2) */
               uid_t          uid;         /* Effective UID of owner */
               gid_t          gid;         /* Effective GID of owner */
               uid_t          cuid;        /* Effective UID of creator */
               gid_t          cgid;        /* Effective GID of creator */
               unsigned short mode;        /* Permissions */
               unsigned short __seq;       /* Sequence number */
           };
  1. 信号量
c 复制代码
This function has three or four arguments, depending on cmd.  When there are four, the fourth has the type union semun.  The calling  program must define this union as follows:

           union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
           };

       The semid_ds data structure is defined in <sys/sem.h> as follows:

           struct semid_ds {
               struct ipc_perm sem_perm;  /* Ownership and permissions */
               time_t          sem_otime; /* Last semop time */
               time_t          sem_ctime; /* Last change time */
               unsigned long   sem_nsems; /* No. of semaphores in set */
           };

       The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):

           struct ipc_perm {
               key_t          __key; /* Key supplied to semget(2) */
               uid_t          uid;   /* Effective UID of owner */
               gid_t          gid;   /* Effective GID of owner */
               uid_t          cuid;  /* Effective UID of creator */
               gid_t          cgid;  /* Effective GID of creator */
               unsigned short mode;  /* Permissions */
               unsigned short __seq; /* Sequence number */
           };

信号

  • 注:信号和信号量是没有任何关系的,切记不要将两者混淆。

  • 在Linux系统中都有哪些信号呢?

  • 我们将信号划分为4部分:预备、信号产生、信号保护、信号处理

1.预备

生活中有哪些信号

  • 举例:发令枪、闹钟、红绿灯、手机铃声、烽火台狼烟

  • 这些信号都有哪些特点呢,我们以红绿灯为例,如下图所示

将生活中信号的概念迁移到进程

  1. 共识:信号是给进程发送的。 例如:kill -9 pid,pid为对应进程的pid
  2. 进程是如何识别信号的呢?
  • 也是通过认识+动作来识别信号的
  1. 进程本身是被程序员编写的属性和逻辑的集合 ====> 这是通过程序员编码完成的
  2. 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理。
  3. 进程本身必须要有对信号的保存能力。
  4. 信号被处理我们将其称为:信号被捕捉
  • 进程在处理信号的时候,一般有三种动作(默认动作,自定义动作,忽略动作)
  1. 思考:如果一个信号是发给进程的,而进程要保存,那么应该保存在哪里?
  • 进程会将信号保存到PCB(也就是task_struct)中
  1. 那么进程是如何保存的信号,怎样判断进程是否收到了指定信号 (如[1,31]的普通信号)

2.信号产生

第一种产生信号的方式(通过终端按键产生信号)

  1. ctrl+c: 是一个热键,本质是一个组合键,OS会将其解释为2号信号,也就是2) SIGINT

  2. ctrl+\:是一个热键,本质是一个组合键,OS会将其解释为2号信号,也就是3) SIGQUIT

  3. man 7 signal, 7 号手册为详细手册

c 复制代码
// 在7号手册中,我们可以查询到每个信号的功能
// Term --> termination n.终止,结束
// SIGINT        2       Term    Interrupt from keyboard
// nterrupt from keyboard --> 可以将其解释为,通过键盘输入来使进程终止
First the signals described in the original POSIX.1-1990 standard.

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

signal()

c 复制代码
功能:signal - ANSI C signal handling

// 头文件
#include <signal.h>

// 函数
typedef void (*sighandler_t)(int); // 函数指针

sighandler_t signal(int signum, sighandler_t handler); // 函数

// 参数
int signum:代表的是对应的信号编号

makefile

c 复制代码
mysignal:mysignal.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mysignal

mysignal.cc

c 复制代码
// 情况一:
#include <iostream>
#include <unistd.h>

int main()
{
    while(true)
    {
        std::cout << "我是一个进程: " << getpid() << std::endl;
        sleep(1);
    }
}
// 运行效果如下图所示

// 情况二
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "进程捕捉到了一个信号,信号编号是: " << signo << std::endl;
    
    // 如果我们想要收到信号后,退出当前进程,这里我们可以设置exit(0)
    // exit(0);
}

int main()
{
    // 这里是signal函数的调用,并不是handler的调用
    // 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
    // 一般这个方法不会执行,除非收到对应的信号
    signal(2, handler);

    while(true)
    {
        std::cout << "我是一个进程: " << getpid() << std::endl;
        sleep(1);
    }
}
// 运行效果如下图所示
  • 情况一
  • 情况二

第二种产生信号的方式(调用系统函数向进程发送信号)

创建一个kill命令

  1. 我们平时在shell中使用的kill命令来杀死某个进程,其实kill命令是调用了操作系统提供的接口kill(),那么我们也是可以通过调用这个接口来实现和kill命令一样的功能的。

main()

c 复制代码
int main(int argc , char* argv[],char* envp[]);

/*
参数说明:
  第一个参数argc表示的是传入参数的个数
  第二个参数char* argv[],是字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。各成员含义如下:
  argv[0]:指向程序运行的全路径名
  argv[1]:指向执行程序名后的第一个字符串 ,表示真正传入的第一个参数
  argv[2]:指向执行程序名后的第二个字符串 ,表示传入的第二个参数
   ......
  argv[n]:指向执行程序名后的第n个字符串 ,表示传入的第n个参数
  
规定:argv[argc]为NULL ,表示参数的结尾。
*/

kill()

c 复制代码
功能: kill - send signal to a process

// 头文件
#include <sys/types.h>
#include <signal.h>

// 函数
int kill(pid_t pid, int sig);

// 参数
pid_t pid :对应进程的pid
int sig :所要发送的信号编号    

makefile

c 复制代码
.PHONY:all
all:mysignal mytest

mytest:mytest.cc
	g++ -o $@ $^ -std=c++11

mysignal:mysignal.cc
	g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
	rm -f mysignal mytest

mytest.cc

c 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>

//我写了一个将来会一直运行的程序,用来进行后续的测试
int main()
{
    while(true)
    {
        std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
        sleep(1);
    }
}

mysignal.cc

c 复制代码
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>

using namespace std;

static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " pid signo\n" << std::endl;
}


// ./myprocess pid signo
int main(int argc, char *argv[])
{
    // 2. 系统调用向目标进程发送信号
    // kill()可以想任意进程发送任意信号
    // 如果我们没有向进程传递参数pid和signo,那么argc !=3,就会进入这个if语句
    // 只有当参数pid和signo都被传递,那么argc != 3才为假
    // 因为./mysignal pid signo 一共是3个参数
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 将数组argv[]中的字符串转为整型
    // int atoi (const char * str) 的功能就是将字符串转为整型,并将其返回
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);

    int n = kill(pid, signo);
    if(n != 0)
    {
        perror("kill");
    }
  
}
// 演示如下

raise()

c 复制代码
功能:raise - send a signal to the caller

// 头文件
#include <signal.h>

// 函数
int raise(int sig);

mysignal.cc

c 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

// ./myprocess pid signo
int main(int argc, char *argv[])
{
    // 2. 系统调用向目标进程发送信号
    // raise() 给自己 发送 任意信号
    int cnt = 0;
    while(cnt <= 10)
    {
        printf("cnt: %d, pid: %d\n", cnt++, getpid());
        sleep(1);
        
        if(cnt >= 5) 
            raise(3); // 给当前进程发送3号信号
        
        //SIGQUIT       3       Core    Quit from keyboard

    }
   
}
// 演示如下

abort()

c 复制代码
功能:abort - cause abnormal process termination

// 头文件
#include <stdlib.h>
    
// 函数
void abort(void);

mysignal.cc

c 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

// ./myprocess pid signo
int main(int argc, char *argv[])
{
    // 2. 系统调用向目标进程发送信号
    // abort() 给自己 发送 指定的信号 ==> 也就是给当前进程发送6号信号 ==> 6) SIGABRT, 
    // 可以使用kill接口来模拟实现 ==> kill(getpid(), SIGABRT)
    int cnt = 0;
    while(cnt <= 10)
    {
        printf("cnt: %d, pid: %d\n", cnt++, getpid());
        sleep(1);
        
       if(cnt >= 5) abort(); 
        
        // SIGABRT       6       Core    Abort signal from abort(3)

    }
   
}
// 演示如下

注:

关于信号处理的行为的理解 :有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程。

**信号的意义:**信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样。

第三种产生信号的方式(硬件异常产生信号)

除0错误

mysignal.cc

c 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

int main(int argc, char *argv[])
{
    // 3. 产生信号的方式:硬件异常产生信号
    // 信号产生,不一定非得用户显示的发送!
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        
        // 为什么除0 会终止进程?
        // 这是因为当前进程会受到来自OS系统的信号 ==> 8) SIGFPE	
        // SIGFPE        8       Core    Floating point exception
        a /= 0;
    } 
   
}
// 演示如下

那么如何证明我们上述所说的呢?

c 复制代码
// mysignal.cc

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}

int main(int argc, char *argv[])
{
    // 3. 产生信号的方式:硬件异常产生信号
    // 信号产生,不一定非得用户显示的发送!

    // 将OS发送给进程的信号进行捕捉,执行我们自定义的操作
    signal(SIGFPE, catchSig);

    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        int a = 10;
        a /= 0;
        sleep(1);
  
    } 
   
}
// 演示如下
  • CPU内部

野指针错误

c 复制代码
// mysignal.cc

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

int main(int argc, char *argv[])
{
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        p = nullptr; 
        *p = 100; 
    }
}
// 演示如下
  • 对上述所说进行验证
c 复制代码
// mysignal.cc

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
    exit(1)
}

int main(int argc, char *argv[])
{
    // 3. 产生信号的方式:硬件异常产生信号
    // 信号产生,不一定非得用户显示的发送!
    
    // 将OS发送给进程的信号进行捕捉,执行我们自定义的操作
    signal(11, catchSig);

    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        p = nullptr; 
        *p = 100; 
    }
   
}
// 演示如下
  • 那么操作系统是如何知道进程出现了野指针呢?

第四种产生信号的方式( 由软件条件产生信号)

alarm()

c 复制代码
功能:alarm - set an alarm clock for delivery of a signal  // 设置发送信号的闹钟

// 头文件
#include <unistd.h>

// 函数
unsigned int alarm(unsigned int seconds);

// 描述
// SIGALRM 是14号信号
    调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
        
// 返回值
alarm()  returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm.        
    // Alarm()返回任何先前计划的警报需要交付的剩余秒数,如果没有先前计划的警报,则返回零。
  • 以下代码可以验证alarm()的返回值问题
c 复制代码
#include <iostream>
#include <unistd.h>

int main(int argc, char *argv[])
{
    alarm(10);

    int cnt = 0;
    while(true)
    {
        cnt++;
        if(cnt == 2)
        {
            sleep(1);
            break;
        }
    }
    
    int a = alarm(3);
    cout << a << endl;  
   
}
[qwy@VM-4-3-centos lesson26]$ ./mysignal 
9     // 返回值为9

mysignal.cc

c 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

int cnt = 0;

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo <<"\n累加次数是: " << cnt << std::endl;
    exit(1);
}

int main(int argc, char *argv[])
{
    // 4. 软件条件 -- "闹钟"其实就是用软件实现的
    signal(SIGALRM, catchSig);

    // 在一秒钟之后,alarm会向当前进程发送14号信号  ==> 14) SIGALRM	
    // 作用:统计1S左右,我们的计算机能够将数据累加多少次
    alarm(1);
    while(true)
    {
        cnt++;
    }
   
}

// 运行结果
[qwy@VM-4-3-centos lesson26]$ ./mysignal 
获取到一个信号,信号编号是: 14
累加次数是: 507652956

总结思考一下

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候
  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

核心转储问题

  • term终止进程和core终止进程有什么不同?
  1. 在云服务器上,默认进程如果是core退出的,我们暂时看不到明显现象,如果想要看到:
  • 因为云服务器默认关闭了core file选项
  • 因此我们需要打开core file选项,并设置合适的存储空间

mysignal.cc

c 复制代码
#include <iostream>

int main(int argc, char *argv[])
{
    // 核心转储
    while (true)
    {
        int a[10];

        // 此处,数组发生了越界
        a[10000] = 321;
    }  
}
// 演示如下
  • 通过错误信息文件,我们就可以快速直到进程为什么崩溃,是在哪里崩溃的

管理员信号(9号信号)

c 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}

int main(int argc, char *argv[])
{
    for(int signo = 1; signo <= 31; signo++)
    {
        signal(signo, catchSig);
    }

    while(true) 
    {
        cout << "我在运行: " << getpid() <<endl;
        sleep(2);
    }

    return 0;
}

3.信号保存

阻塞信号

  1. 信号其他相关的常见概念
  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号在内存中的示意图

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号

产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子

中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前

不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次

或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可

以依次放在一个队列里(我们这里不说实时信号)。

注:对上述内容进行更详细的解读,见下图:

4.信号处理

内核态

  • 第一部分
  • 第二部分
  • 第三部分

信号的捕捉流程

  • 总结上面的流程图之后,再来看下面的这张流程图

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统

实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做

任何解释,比如printf直接打印sigset_t变量是没有意义的。

c 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有

效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit位都被置为1,表示该信号集的有效信号包括系统支持的所有信号。

  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。

  • 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask()

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

c 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
// 返回值:若成功则为0,若出错则为-1

// 参数
const sigset_t *set
    // 输入型参数,假设我们需要将所有的信号都置为1(也就是对所有的信号做屏蔽),那么我们就调用sigfillset(sigset_t *set)将set位图对应的bit位都置为1,再调用sigprocmask()进行批量设置就可以了
    
sigset_t *oset
    // 输出型参数,当我们对信号屏蔽字做重置时,我们需要先将老的信号屏蔽字保存到oset进行返回,当我们想要恢复老的信号屏蔽字,则读取oset就可以了

如果oset是非空指针,则读取进程的当前信号屏蔽字 通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当Fmask=mask | set ( | 按位或)
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当Fmask=mask & ~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set
  • SIG_BLOCK:批量添加,将set中的信号屏蔽字批量添加到block表中
  • SIG_UNBLOCK:批量删除,将set中的信号屏蔽字从block表中批量删除
  • SIG_SETMASK:批量重置,将set中的信号屏蔽字全部重置到block表中

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递

达。

sigpending()

c 复制代码
功能:sigpending - examine pending signals :检查挂起信号
     
// 头文件
#include <signal.h>

// 函数
 int sigpending(sigset_t *set);
	// 如果哪个进程调用sigpending(sigset_t *set),那么就返回哪个进程的pending位图

// 参数
sigset_t *set  // 输出型参数
  // 将调用进程的pending位图,放置在set中

// 返回值
sigpending() returns 0 on success and -1 on error.  In the event of an error, errno is set to indicate the cause.
    // Sigpending()成功时返回0,出错时返回-1。如果发生错误,则设置errno来指示原因。

演示代码

  1. 默认情况下:我们的所有信号都是不被阻塞的
  2. 默认情况下:如果一个信号被屏蔽了,该信号不会被递达

makefile

c 复制代码
mysignal:mysignal.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mysignal

mysignal.cc

c 复制代码
#include <iostream>
#include <vector>
#include <signal.h>
#include <unistd.h>

// 使用宏将信号的最大数量31,定义为MAX_SIGNUM
#define MAX_SIGNUM 31

using namespace std;

// 当我们想屏蔽对多个信号进行屏蔽,我们只需要向vector中添加对应的信号编号即可
// static vector<int> sigarr = {2,3};

// 目前,只演示对于2号信号的屏蔽
static vector<int> sigarr = {2};

// 打印我们获取的当前进程的pending表
static void show_pending(const sigset_t &pending)
{
    // 一共31个信号,依次进行遍历
    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    {
        // sigismember() 是用来判断,signo是否为pending信号集中的有效信号
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else cout << "0";
    }
    cout << "\n";
}

// 自定义的handler方法
static void myhandler(int signo)
{
    cout << signo << " 号信号已经被递达" << endl;
}

int main()
{
    // 只要收到sigarr中的信号,就会按我们自定义的handler方法进行处理
    for(const auto &sig : sigarr) signal(sig, myhandler);

    // int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
    // block对应set,我们可以将block的对应的位图,设置到我们进程对应的block表中
    // oblock对应oset,设置新的block时,会将旧的block表保存到oset中,oset为输出型参数
    // int sigpending(sigset_t *set);
    // pending对应sigpending()中的set参数,会将调用sigpending()的进程的pending表保存到set中
    // 1. 先尝试屏蔽指定的信号
    sigset_t block, oblock, pending;
    // 1.1 初始化
    // 将用户级,我们自定义的位图结构先做初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    for(const auto &sig : sigarr) 
        sigaddset(&block, sig);

    // 1.3 开始屏蔽,将我们定义的表的数据设置进内核(进程的task_struct中)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2. 遍历打印pengding信号集
    int cnt = 5;
    while(true)
    {
        // 2.1 初始化
        sigemptyset(&pending);
        // 2.2 获取它
        sigpending(&pending);
        // 2.3 打印它
        show_pending(pending);
        // 3. 让打印的速度慢一点,便于我们进行观察
        sleep(1);

        // 运行5秒后,解除对信号的阻塞
        if(cnt-- == 0)
        {
            // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号
            sigprocmask(SIG_SETMASK, &oblock, &block); 

            // 如果我们没有使用signal(sig, myhandler)对siggar中的信号进行捕捉
            // 那么代码无法运行到这里,因为sigprocmask()调用之后,2号信号被递达,进程终止
            cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
        }
    }
}
// 演示如下图所示

sigaction()

c 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
c 复制代码
 struct sigaction {
               void     (*sa_handler)(int);    // 我们自定义的handler处理方法
               void     (*sa_sigaction)(int, siginfo_t *, void *); // 设置为null或不处理
               sigset_t   sa_mask;             // 我们需要屏蔽的屏蔽字的位图结构对象
               int        sa_flags;            // 设置为0
               void     (*sa_restorer)(void);  // 设置为null
           };
  • sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,**赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。**显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

  • 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字 ,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

  • sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,不作说明

makefile

c 复制代码
mysignal:mysignal.cc
	g++ -o $@  $^  -std=c++11

.PHONY:clean
clean:
	rm -f mysignal

mysignal.cc

c 复制代码
#include <iostream>
#include <cstdio>
#include <signal.h>
#include <unistd.h>

using namespace std;

void Count(int cnt)
{
    while(cnt)
    {
        // 在C语言中,"\r"是一个转义字符,表示回车符(Carriage Return)。
        // 当在字符串中使用"\r"时,它会将光标移动到当前行的开头位置,以实现回车的效果。
        // 这通常与换行符"\n"一起使用,形成回车换行的组合,用于在控制台或文本文件中换行输出文本。
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

void myhandler(int signo)
{
    cout << "get a signo: " << signo << "正在处理中..." << endl;
    Count(10);
}

int main()
{
    // int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
    // act,oact都是sigaction()的参数,且这两个参数的类型都为结构体 struct sigaction
    // act是输入型参数, oact为输出型参数
    struct sigaction act, oact;

    // 自定义的信号处理方式
    act.sa_handler = myhandler;  

    // sa_flag默认设置为0
    act.sa_flags = 0;

    // 对act.sa_mask进行初始化
    sigemptyset(&act.sa_mask); 

    // 当递达其他信号期间,在act.sa_mask中的信号都会被阻塞
    // 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
    sigaddset(&act.sa_mask, 3);

    // SIGINT是2号信号定义的宏
    // 将act.sa_handler自定义的处理方式设置到内核表handler中
    // 就将内核表handler的旧的处理方法,存放到oact中
    // 因为我们调用sigaction(),只是针对一个信号(就是我们传入的2号信号)
    // 因此当我们将3号信号添加到act.sa_mask里面,但是3号信号并不会执行我们自定义的处理方法
    // 依旧是采用默认的处理方式
    sigaction(SIGINT, &act, &oact);    

    while(true) sleep(1);

    return 0;
}

5.可重入函数

  1. 一般而言,我们认为:mian执行流和信号捕捉执行流是两个执行流。
  2. 如果在main中和在handler中,该函数被重复进入,出问题,我们称函数insert为不可重入函数。
  3. 如果在main中和在handler中,该函数被重复进入,没有出问题,我们称函数insert为可重入函数。
  4. 不可重入函数是特性,是一个中性词,我们目前大部分情况下使用的接口都是不可重入的。
  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入, insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数 ,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。

  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

6.volatile

makefile

c 复制代码
mysignal:mysignal.c
	gcc -o $@  $^ -O3        # -O3 为优化等级
.PHONY:clean
clean:
	rm -f mysignal

mysignal.c

c 复制代码
#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo)
{
    printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
    printf("quit: %d", quit);
    quit = 1;
    printf("-> %d\n", quit);
}

int main()
{
    signal(2, handler);
    while(!quit);
    printf("注意, 我是正常退出的!\n");
    return 0;

    return 0;
}
  • 情况一:在编译器没有进行优化时(也就是makefile中没有 -O3的优化选项时),运行结果如下
  • 情况二:在编译器进行优化时(也就是makefile中有 -O3的优化选项时),运行结果如下

使用volatile来解决如上问题

c 复制代码
 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// volatile: 保持内存可见性
// 也就是说,虽然quit不会在main执行流中被修改,
// 但是不要将quit值直接优化到寄存器中,而是每一次检测都需要在内存中读取
// 要保证内存的可见性,而不是让寄存器中的值代替物理内存中的quit值
volatile int quit = 0;

void handler(int signo)
{
    printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
    printf("quit: %d", quit);
    quit = 1;
    printf("-> %d\n", quit);
}

int main()
{
    signal(2, handler);
    while(!quit);
    printf("注意, 我是正常退出的!\n");
    return 0;
}

7.SIGCHLD信号(了解内容)

什么是SIGCHLD信号

  1. 在Linux系统中,子进程在终止时,会想父进程发送17号信号,也就是SIGCHLD信号
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
}

void Count(int cnt)
{
    while (cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

int main()
{
    // 显示的设置对SIGCHLD进行忽略
 	signal(SIGCHLD, SIG_IGN);

    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
        Count(5);
        exit(1);
    } 

    // 保证父进程不退出
    while (1) sleep(1);

    return 0;
}
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void handler(int sig)
{
    // 情况一:有非常多的子进程,在同一个时刻退出了
    // 情况二:有非常多的子进程,在同一个时刻只有一部分退出了
    // 都可以用一下方案等待子进程回收
 	pid_t id;
    //  pid_t waitpid(pid_t pid, int *status, int options);
    //  WNOHANG: 表示非阻塞等待
    //  -1 : 传对应pid_t只可以回收对应的子进程,而传-1则可以回收任意子进程
 	while(  (id = waitpid(-1, NULL, WNOHANG)) > 0 )
	{
 		printf("wait child success: %d\n", id);
 	}
    
 	printf("child is quit! %d\n", getpid());
}

int main()
{
 	signal(SIGCHLD, handler);
 	pid_t cid = fork();
 	if(cid == 0)
	{
		//child
 		printf("child : %d\n", getpid());
 		sleep(3);
 		exit(1);
 	}
 	while(1)
	{
 		printf("father proc is doing some thing!\n");
 		sleep(1);
 	}
	 return 0;
}

父进程忽略SIGCHLD信号

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>


void Count(int cnt)
{
    while (cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

int main()
{
    // 只在Linux下有用
    // 显示的设置对SIGCHLD进行忽略
    // 当子进程执行完,那么系统会直接回收子进程的资源
 	signal(SIGCHLD, SIG_IGN);
    
    //  SIGCHLD   20,17,18    Ign     Child stopped or terminated
    // 虽然17号信号默认就是ignore,但是这种,子进程依旧会有僵尸状态,依旧会执行handler方法
    // signal(SIGCHLD, SIG_DFL);

    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
        Count(5);
        exit(1);
    } 

    // 保证父进程不退出
    while (1) sleep(1);

    return 0;
}

非常多的子进程,在同一个时刻只有一部分退出了

// 都可以用一下方案等待子进程回收

pid_t id;

// pid_t waitpid(pid_t pid, int *status, int options);

// WNOHANG: 表示非阻塞等待

// -1 : 传对应pid_t只可以回收对应的子进程,而传-1则可以回收任意子进程

while( (id = waitpid(-1, NULL, WNOHANG)) > 0 )

{

printf("wait child success: %d\n", id);

}

复制代码
printf("child is quit! %d\n", getpid());

}

int main()

{

signal(SIGCHLD, handler);

pid_t cid = fork();

if(cid == 0)

{

//child

printf("child : %d\n", getpid());

sleep(3);

exit(1);

}

while(1)

{

printf("father proc is doing some thing!\n");

sleep(1);

}

return 0;

}

复制代码
## 父进程忽略SIGCHLD信号

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


void Count(int cnt)
{
    while (cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

int main()
{
    // 只在Linux下有用
    // 显示的设置对SIGCHLD进行忽略
    // 当子进程执行完,那么系统会直接回收子进程的资源
 	signal(SIGCHLD, SIG_IGN);
    
    //  SIGCHLD   20,17,18    Ign     Child stopped or terminated
    // 虽然17号信号默认就是ignore,但是这种,子进程依旧会有僵尸状态,依旧会执行handler方法
    // signal(SIGCHLD, SIG_DFL);

    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
        Count(5);
        exit(1);
    } 

    // 保证父进程不退出
    while (1) sleep(1);

    return 0;
}
相关推荐
zyx没烦恼33 分钟前
Linux 下 日志系统搭建全攻略
linux·服务器·开发语言·c++
Tee xm1 小时前
清晰易懂的 Flutter 开发环境搭建教程
linux·windows·flutter·macos·安装
不摆烂选手1 小时前
Ubuntu之Makefile入门
linux·ubuntu·makefile·正点原子imx6ull学习笔记
MCYH02062 小时前
C++抽卡模拟器
java·c++·算法·概率·原神
pystraf2 小时前
P10587 「ALFR Round 2」C 小 Y 的数 Solution
数据结构·c++·算法·线段树·洛谷
码上飞扬2 小时前
深入探索 Linux Top 命令:15 个实用示例
linux·运维·服务器
zkyqss2 小时前
OpenStack Yoga版安装笔记(十七)安全组笔记
linux·笔记·openstack
郭涤生2 小时前
The whole book test_《C++20Get the details》_notes
开发语言·c++·笔记·c++20
Jerry说前后端2 小时前
剑指Offer(数据结构与算法面试题精讲)C++版——day6
开发语言·c++·面试
梁下轻语的秋缘2 小时前
每日c/c++题 备战蓝桥杯(求解三个数的最大公约数与最小公倍数)
c语言·c++·学习·算法·蓝桥杯