C++学习笔记day30-----UC-信号的阻塞和未决信号,一个信号从产生到处理完毕的完整过程,进程间的通讯

上篇笔记中提到,信号在到达进程之后,最后由信号处理函数来处理。信号处理函数是可以被子进程继承的。
下面通过一段代码验证:

#include<stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
void doit(int num){
    printf("pid %d recv...%d\n",getpid(),num);
    return;
}
int main(void){
    //向进程注册信号处理函数
    signal(2,doit);
    //创建子进程
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return -1;
    }
    if(pid == 0){//子进程执行的程序

    }
    else{//父进程执行的程序
        //wait(0);
    }
    while(1);
    return 0;
}

执行这段代码的时候,从键盘输入Ctrl+c,会发现两个进程都打印了内容(一个中断发给了两个进程,是因为信号是可以针对进程组发送的),但是都没有结束。说明子进程继承了父进程的信号处理函数。
介绍一个暂停程序的函数:

#include <unistd.h>
int pause(void);
功能:等待一个信号的到来
参数:
void
返回值:
只有等到信号,调用信号处理函数以后,才返回-1,errno被设置

当进程执行到这个函数的时候,就会挂起,直到有信号到达该进程,它才会继续向下执行。
通过alarm(2)函数和pause(2)函数,可以实现一个sleep(3)的功能,代码如下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void doit(int num){
    return;
}
unsigned int t_sleep (unsigned int seconds){
    //设置闹钟
    alarm(seconds);
    //暂停
    pause();
    return alarm(0);
}
int main(void){
    signal(14,doit);
    while(1){
        t_sleep(2);
        printf("hehe\n");
    }
    return 0;
}

信号的阻塞和未决信号
阻塞
当一个信号到达进程之后,先和blocking进行判断,blocking是进程的信号掩码集,在这个集合里的信号会被阻塞不会被进程的信号处理函数处理。pending中存放的是未决信号。无论信号是否被阻塞,都会有未决状态,只是不被阻塞的信号的未决状态极短,基本无法捕捉。而被阻塞的信号,到达进程后,会被存放在未决信号集中。直到进程对该信号的阻塞状态解除,才会调用相应的信号处理函数处理它。如果直到进程结束都没有解除阻塞状态。那么进程永远都不会处理这个信号。
在系统中,blocking和pending被称为信号集类型,这个类型是一个结构体类型,封装了一个长整型,如下:

typedef struct
  {
    unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
  } __sigset_t;
  typedef __sigset_t sigset_t;

系统提供了一系列的函数来支持用户,可以自定义信号掩码集(blocking),然后再通过另外的函数将信号掩码集注册给当前的进程。函数如下:

#include <signal.h>
int sigemptyset(sigset_t *set);
功能:将set信号集设置为空,set里不包含任何信号
参数:
set:指定要初始化的信号集
返回值:
success:0
error:-1,errno被设置


int sigfillset(sigset_t *set);
功能:将set信号集合设置为满,set里包含所有的信号
参数:
set:指定要初始化的信号集
返回值:
success:0
error:-1,errno被设置


int sigaddset(sigset_t *set, int signum);
功能:将信号signum,添加到信号集set中
参数:
set:要操作的信号集
signum:要操作的信号编号
返回值:
success:0
error:-1,errno被设置


int sigdelset(sigset_t *set, int signum);
功能:将信号signum,从信号集set里删除
参数:
set:要操作的信号集
signum:要操作的信号编号
返回值:
success:0
error:-1,errno被设置


int sigismember(const sigset_t *set, int signum);
功能:测试信号是否是信号集的一个成员
参数:
set:执行信号集
signum:指定信号
返回值:
1   信号signum是信号集set里的一个成员
0   不是
-1  错误,errno被设置


使用函数sigprocmask(2)将信号集设置成进程的信号掩码集。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:检测或改变多个阻塞信号(信号集)
参数:
how:
SIG_BLOCK:将set信号集和当前的信号集的并集作为新的信号集
SIG_UNBLOCK:将set信号集里的信号从当前信号集里删除,解除阻塞
SIG_SETMASK:将set信号集设置成当前进程的信号集(掩码)
set:如果是NULL,当前的信号集不改变,并将现在的信号集保存到oldset(如果oldset不为空)。如果不是NULL,根据how来操作。
oldset:如果不是NULL,就用于保存原来的信号集
返回值:
success:0
error:-1 errno

sigpending(2)
#include <signal.h>
int sigpending(sigset_t *set);
功能:检测未决信号
参数:
set:未决信号集存储到set指定的地址空间里
返回值:
success:0
error:-1 errno

通过一段代码来演示上述函数:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void doit(int num){
    return;
}
unsigned int t_sleep(unsigned int num){
    alarm(num);
    pause();
    return alarm(0);
}
int main(void){
    //用于存储信号集
    sigset_t set;
    sigset_t oldset;

    //初始化信号集
    int flag = sigemptyset(&set);
    if(flag == -1){
        perror("sigemptyset");
        return -1;
    }
    //将14号和2号信号加入到信号集
    //sigaddset(&set,14);
    sigaddset(&set,2);
    //将信号集注册给当前进程
    sigprocmask(SIG_SETMASK,&set,NULL);
    //注册信号处理函数
    signal(14,doit);
    //保持进程运行
    while(1){
            //睡眠两秒
        t_sleep(2);
        //将进程当前的未决信号放在oldset集合中
        sigpending(&oldset);
        //在进程当前的未决信号集中寻找是否存在信号编号2的信号
        int s = sigismember(&oldset,2);
        //如果有就打印yes,如果没有就打印no
        s ? printf("yes\n") : printf("no\n");
    }
    return 0;
}

不可靠信号 也称为时时信号
信号在阻塞的时候,向进程多次发送信号,进程解除对信号的阻塞的时候,只处理一次,造成了信号的丢失,这样的信号成为不可靠信号1-31号。

可靠信号
信号在阻塞的时候,向进程多次发送信号,进程解除对信号的阻塞的时候,处理多次,信号没有丢失,这样的信号成为可靠信号34-64号。

一个信号从发生到处理
以2号信号为例。
2号信号可以由键盘输入触发(Ctrl+c)。
按下按键的时候,发生一个硬件中断,这个时候无论进程是在内核态还是在用户态都会切换到内核态。驱动程序会将按键组合解释成2号信号发送给进程,将信号记录到进程的PCB之后,进程切换到用户态,然后查询自己的PCB看看是否有信号,如果有就调用相应的信号处理函数,在信号处理函数结束后,调用sigreturn(2)函数,释放信号处理函数的栈帧,并返回内核态,清楚处理过的信号。再切换到用户态,检查PCB是否含有信号。直到信号处理完毕。

进程间的通讯
首先理一下,之前学习过管道的方式用于进程通讯,这个是进程通讯的原理。市面上有公司(system v ipc)基于原理做了一些优秀的用于进程间通讯的接口。我们直接使用这些安全高效的接口就可以了。
上述公司对于进程间的通讯主要有以下三种方式:
消息队列,共享内存,信号量集
先介绍一个指令,ipcs,可以列出当前系统内的上述三种方式下的内存产物

linxin@ubuntu:~/UC/day10$ ipcs

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 327680     linxin     600        524288     2          目标       
0x00000000 294913     linxin     600        16777216   2          目标       
0x00000000 393218     linxin     700        16472      2          目标       
0x00000000 688131     linxin     700        205128     2          目标       

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems 

上述三种方式,是有唯一id的,例如msqid(消息队列)、shmid(共享内存)、semid(信号量集)
由于进程间的通讯使用的内存是在内核态(再讲管道的时候解释过),所以进程的用户态是无法直接操作这些内容,只能通过system call 或者库函数来完成。
当程序想要对其中一种方式的内存进行操作的时候,比如向队列写入数据,从队列读取数据这样的操作的时候,必须要知道这个队列的id。
获取队列的id也需要通过system call。而获取这种id要通过进程的key(键)来获取。
进程的key具有唯一性,key和队列进行绑定。系统提供了以下函数,来创建一个进程的key

获取进程的键值(唯一性)
ftok(3)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:将pathname,proj_id的值转换为system v ipc key
参数:
pathname:指定一个有效的路径
proj_id:必须为非0,指定一个整数,使用该整数的低有效8位。
返回值:
success:返回生成的key_t类型的键值
error:-1,errno被设置

如果两个参数的值,完全一样,多次调用ftok的返回值一样。

现在进程已经有唯一的key了,通过key来绑定并访问消息队列
消息队列
通过进程的key,来绑定并返回消息队列的ID,通过以下函数:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:获取一个消息队列的id
参数:
key:指定键值,如果没有和key相关的消息队列,就创建消息队列,并返回消息队列的ID,如果有消息队列,仅返回该消息队列的ID
msgflag:
IPC_CREAT,创建
IPC_EXCL,和IPC_CREAT连用的时候,如果已经存在和key值相关的消息队列,函数会失败,errno被设置
mode:指定了消息队列的权限,这个权限和文件的权限一样
返回值:
success:返回消息队列的ID,一个非负的整数
error:-1,errno被设置

到这一步,消息队列才被创建!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值