Linux 进程间通讯(第三阶段 信号 信号量)

12.信号概述

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
原文摘自:https://www.jianshu.com/p/f445bfeea40a
1、信号的名字和编号:
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
在这里插入图片描述
2、信号的处理:
信号的处理有三种方法,分别是:忽略、捕捉和默认动作

1、忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景

2、捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

3、系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。

了解了信号的概述,那么,信号是如何来使用呢?

其实对于常用的 kill 命令就是一个发送信号的工具,kill -9 PID来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。
在这里插入图片描述
对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段(就是信号的处理方式第二种捕捉信号);

其他知识点(Ctrl+c等命令是shell命令)

13.信号编程

信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版

1、入门版:函数signal
2、高级版:函数sigaction

信号处理发送函数
信号发送函数也不止一个,同样分为入门版和高级版
1.入门版:kill
2.高级版:sigqueue

下面看看函数的原型:
typedef void (*sighandler_t)(int);//函数指针,返回值为无
sighandler_t signal(int signum, sighandler_t handler);//第一个参数就是信号可以用kill -l查看哪个(shell指令),第二个参数就是上面的函数指针了。

下面是输入Ctrl + c 这个信号让进程不会退出的代码(signalDemo1.c)

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

//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)//收到信号(ctrl + c)让它执行我们希望执行的这个函数,而不是执行原有的默认动作。
{
        printf("get signum=%d\n",signum);
        printf("never quit\n");
}

int main()
{
        signal(SIGINT,handler);//注册信号

        while(1);
        return 0;
}

下图就是捕捉到信号(ctrl +c)不是执行其默认的动作,而是修改器动作的代码执行结果:(最后还是需要kill 9 pid来终止这个进程)
在这里插入图片描述
以下的代码是注册其他的信号代码(signalDemo1.c):

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

//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)//收到信号执行其他内容
{
        printf("get signum=%d\n",signum);
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        printf("never quit\n");
        }
}

int main()
{
        signal(SIGINT,handler);//注册ctl+c的信号
        signal(SIGKILL,handler);//注册信号
        signal(SIGUSR1,handler);//注册信号

        while(1);
        return 0;
}

运行结果如下:SIGKILL不会被信号捕捉后执行我们想让他执行的动作,而是还是原来默认的功能,杀死进程,如下图:
在这里插入图片描述
现在写一个用程序(signalCON.c)来对上面的进程(signalDemo1.c)发送信号,上面的都是用键盘输入的信号,这里用到的函数是kill:

#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

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

int main(int argc,char **argv)
{
        int signum;
        int pid;

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);//把参数传进来

        printf("num=%d,pid=%d\n",signum,pid);

        //kill(pid,signum);//两种方式发信号给上面的进程用kill发送信号给对应的pid进程
        sprintf(cmd,"kill -%d %d",signum,pid);//sprintf函数的使用
        system("cmd");//这里用system调用脚本给signalDemo1.c传信号
        printf("send signal ok\n");
        return 0}

下图是把输入对应的pid值和信号的值,然后通过程序给signalDemo1.c的进程发送信号:
在这里插入图片描述
以下是用忽略信号的宏来对ctrl+c信号的操作,代码(修改后的signalDemo1.c)如下:

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

//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)
{
        printf("get signum=%d\n",signum);
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        printf("never quit\n");
        }
}

int main()
{
        signal(SIGINT,SIG_IGN);//SIG_IGN忽略信号
        signal(SIGKILL,SIG_IGN);//杀死进程忽略不了。
        signal(SIGUSR1,handler);

        while(1);
        return 0;
}

运行结果如下图:
在这里插入图片描述

14.信号如何携带消息

入门是对发来的信号做的动作,高级的话就是发过来的信号带有消息;之前场景的话类似只敲门不讲话;现在高级的是敲门带有讲话,带有信息。
下图是携带消息编程前的思路图:
在这里插入图片描述
详细文字解答如下:接收端及发送端
信号注册(接收端)函数——高级版

sigaction 的函数原型

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);//第一个是信号的值,第二个参数是信号处理结构体,第三个参数是信号备份的

struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作(用这个参数和signal没有区别,没办法处理消息)
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一

这个函数的原版帮助信息,可以通过man sigaction来查看

a、sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum应该就是注册的信号的编号;第二个参数act如果不为空说明需要对该信号有新的配置;第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

b、在这里额外说一下struct sigaction结构体中的 sa_mask 成员,起到的是阻塞作用,不配置,默认就是阻塞的;

c、关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用 sa_sigaction 这个函数指针所指向的信号处理函数。

关于void (*sa_sigaction)(int, siginfo_t *, void );处理函数来说还需要有一些说明。int是记录发信号的值是多少;void 是接收到信号所携带的额外数据,可以判断其是否为空,空的话没有信息携带可以从siginfo里读,非空就可以从从siginfo读取到信息;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据

信号发送函数——高级版
发送信号函数sigqueue原型:

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);//第一个参数是发给谁pid;第二个参数是什么信号;第三个参数要发送的消息
union sigval {
   int   sival_int;//整形
   void *sival_ptr;//字符串
 };

15.信号携带消息编程实战

接收端代码(NiceSignal.c):

#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
void handler(int signum, siginfo_t *info, void *context)//对收到的信号处理程序,能够接受额外数据和sigqueue配合使用
{
        printf("get signum %d\n",signum);

        if(context != NULL){//用context是否为空判断有没有信息传过来
                printf("get data=%d\n",info->si_int);//打印发送端的信号携带信息。
                printf("get data=%d\n",info->si_value.sival_int);//info结构体里面的value也是一个结构体,value里面的int型和info的结构体的int是一样的。
                printf("from:%d\n",info->si_pid);//打印info结构体里面的发送端pid值
        }
}

int main()
{
        struct sigaction act;//定义一个信号处理的结构体。
        printf("pid = %d\n",getpid());

        act.sa_sigaction = handler;//配置信号处理函数
        act.sa_flags = SA_SIGINFO;//flags配置这个宏是能够接收到信号携带的信息(be able to get message)

        sigaction(SIGUSR1,&act,NULL);//接收(注册)信号的函数
        while(1);

        return 0;
}

发送端代码(SignalSend.C):

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

//int sigqueue(pid_t pid, int sig, const union sigval value);

int main(int argc,char **argv)
{
        int signum;
        int pid;

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        union sigval value;//发送信号时携带的信息放在这个结合体里,和信号接收端的siginfo_t结构体中的结构体sigval_t si_value对应的。
        value.sival_int = 100;//发送信号时携带的信息配置

        sigqueue(pid,signum,value);//发送信号及携带的信息。
        printf("%d done\n",getpid());

        return 0;
}

下图是信号的接收端与发送端的运行结果:
在这里插入图片描述

16.信号量概述

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
临界资源:
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
信号量的简单介绍及场景图:
在这里插入图片描述

17.信号量编程实现一、二

信号量特点:
1、信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2、信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3、每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4、支持信号量组。
原型:

	最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
	Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);  
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);

当****semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
在**semctl函数中的命令(cmd第三个参数)有多种,这里就说两个常用的:

SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
下面是父子进程用信号量可以控制哪一个先运行的代码(sem.c):(之前我们让子进程先运行时父进程wait或者sleep)现在用信号量:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.h>

//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);//操作信号量的函数,这个函数有三个值或四个值,>取决于cmd;第一个参数是信号量的返回值,第二个是操作信号量集的第几个参数,第三个参数是很多的宏
//int semop(int semid, struct sembuf *sops, size_t nsops);//d第二个参数是结构体数组

union semun {                                     //semctl这个函数的第四个参数(这个联合体)有无>取决于第三个参数宏的调用,
    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) */
};

void pGetkey(int id)//拿锁函数
{
        struct sembuf set;

        set.sem_num = 0;
        set.sem_op = -1;//拿钥匙
        set.sem_flg = SEM_UNDO;//拿不到锁阻塞这

        semop(id, &set ,1);
        printf("getkey\n");

}

void vPutBackKey(int id)//放锁操作
{
        struct sembuf set;

        set.sem_num = 0;//信号量的编号
        set.sem_op = +1;//
        set.sem_flg = SEM_UNDO;//
        
        semop(id, &set ,1);
        printf("put back the key\n");

}

int main(int argc,char const *argv[])
{
        key_t key;
        int semid;

        key = ftok(".",2);
                           //1代表信号量集合中有一个信号量
        semid = semget(key,1,IPC_CREAT|0666);//获取或者创建信号量

        union semun initsem;//定义联合体
        initsem.val = 0;//设置等于0,代表没锁的状态 (=1代表当前是有锁的,可以被进程拿)       
                                //0代表操作第0个信号量
        semctl(semid,0,SETVAL,initsem);//这个函数是初始化信号量
                                //SETVAL设置信号量的初值(在函数第四个参数)这个宏,设置为initsem里的值(设置有一把锁)

        int pid = fork();
        if(pid > 0){
                pGetkey(semid);//父进程拿锁
                printf("this is father\n");
                vPutBackKey(semid);//放锁
                semctl(semid,0,IPC_RMID);//销毁锁,semctl函数就只要三个参数
        }else if(pid == 0){
                printf("this is child\n");
                vPutBackKey(semid);//子进程先运行然后放了一把锁
        }else {
                printf("fork error\n");

        }

        return 0;
}

代码运行结果:都是子进程先运行;
在这里插入图片描述
有个例子,使用了【共享内存+信号量+消息队列】的组合来实现服务器进程与客户进程间的通信。
在这个博文:https://www.cnblogs.com/zgq0/p/8780893.html

五种通讯方式总结(没有信号)

1.管道:速度慢,容量有限,只有父子进程能通讯

2.FIFO:任何进程间都能通讯,但速度慢

3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

4.信号量:不能传递复杂消息,只能用来同步

5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值