概述
一、消息队列
1、本质:
内核中的一个队列,多个进程通过向同一个队列添加节点和获取节点,传输一个有类型(优先级)的数据块。
//用户自己定义这个结构体的内容
struct msgbuf
{
long mtype; /* message type, must be > 0 ,优先级*/
char mtext[1]; /* message data ,数据块大小*/
};
2、接口:
//创建消息队列
msgget();
//添加节点
msgsnd();
//获取节点
msgrcv();
//操作-删除消息队列(IPC_RMID)
msgctl();
3、特性:
- 自带同步与互斥
- 生命周期随内核
- 数据传输自带优先级
二、信号量:
信号量用于实现进程间的互斥与同步,是对系统中资源及其组织情况的抽象,由一个记录型(或结构体类型)数据表示。它包含两个数据项:
第一个为value,表示可用资源数目:
- S.value>0时,表示有value个可用资源;
- S.value=0时,表示资源正好用完;
- S.value<0时,表示有-value个进程正在等待此类资源。
第二个为L,为等待此类资源的进程PCB队列。
在访问临界资源之前先获取信号量,S.value - 1,减完之后判断:
(1)若S.value > 0则进程可以对临界资源进行访问,并且在访问期间,已经将临界资源的状态置为不可访问状态,这样可以保证其他进程此时不会访问这个临界资源,访问完毕之后,S.value+1,若S.value <= 0,则唤醒一个进程(将一个进程从L中出队,置为运行状态)。
(2)若S.value <= 0则使进程等待(将进程加入L队列中);
三、信号:
1、信号产生的方式
进程信号就是一个软件中断,通知进程发生了某个事件,打断进程此刻的操作,去处理这个事件。在操作系统中,信号是多种多样的,一个信号对应一个事件,这样就保证了进程收到某个信号后就知道要去处理什么事件,但是要保证这个信号能被识别。
1、硬件:
ctrl +z(进程中断,后台挂起)
ctrl + c(进程中断)
ctrl + |(进程中断,产生coredumped文件)
2、软件:
int kill(pid_t pid,int signum);
//(#include<signal.h>)给指定进程发送指定的信号,kill命令就是通过它实现的。
int raise(int signum);
//(#include<signal.h>)给当前进程发送指定信号。
void abort(void);
//(#include<stdlib.h>)和exit函数一样,总是能成功。
unsigned int alarm(unsigned int seconds);
//(#include<unistd.h>)定一个闹钟,在seconds秒后给当前进程发送一个SIGALARM信号,该信号默认处理是终止当前进程。
int sigqueue(pid_t pid,int sig,const union sigval value);
//给任意进程(pid)发送任意信号(sig),额外带一个参数value。
2、信号的种类:
通过kill -l命令查看信号的种类,共有62种信号,1-31种是非可靠信号,34-64是可靠信号。
信号表:
解释几个常见的信号:
(1)SIGHUP ---从终端上发出的结束信号
(2)SIGINT ---键盘中断信号//ctrl + c
(3)SIGQUIT ---键盘的退出信号 //ctrl +
(8)SIGFPE ---浮点异常信号(例如浮点运算溢出)
(9)SIGKILL ---该信号结束接收信号的进程;
(14)SIGALRM ---进程的定时器到期时,发送该信号
(15)SIGTERM ---kill命令发出的信号
(17)SIGCHLD ---标识子进程停止或结束的信号
(19)SIGSTOP ---停止执行信号
(20)SIGTSTP ---来自键盘或调试程序的停止执行信号//ctrl + z
3、信号的注册
在pcb中有一个未决信号集合pending,信号的注册就是在这个64位数的每一位上置1即可。
1.非可靠信号(1-31)的注册: 若信号未注册(pending位图的那一位不为1)时,则注册,添加一个sigqueue节点 若信号已经注册,则什么都不做。
2.可靠信号(34-64)的注册:若信号未注册(pending位图的那一位不为1)时,则注册,添加一个sigqueue节点若信号已经注册,则再添加一个sigqueue节点。
4、信号的注销:
1.非可靠信号(1-31)的注销: 将dending位图的那一位置0,并删除要处理sigqueue节点
2.可靠信号(34-64)的注销: 将dending位图的那一位置0,并循环删除要处理的sigqueue节点,直至删完
由此可以看出,不可靠信号只接受一次信号,可能会丢失进程的事件。
4、信号的捕捉处理
分为三种处理方式:
1、默认处理:SIG_DEL
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:
- 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
- 退出(exit):不产生core文件,直接终止进程。
- 忽略(ignore):忽略该信号。
- 停止(stop):挂起该进程。
- 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。
需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。 信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。
2、忽略处理:SIG_IGN
进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
3、自定义处理:void sigcb(int signum) //自定义处理函数
通过下面两种接口修改信号的处理方式:
1.修改信号的回调函数
sighandler_t signal (int signal,sighandler_t )handler);
//signal ---信号值
//handler---操作句柄(函数指针)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sigcb(int signum)
{
printf("signum = %dn",signum);
}
int main()
{
//sighandle_t signal(int signum,sighandler_t handler);
signal(SIGINT,sigcb);
signal(SIGQUIT,SIG_IGN);
while(1)
{
printf("hellon");
sleep(1);
}
return 0;
}
程序运行起来,ctrl+c(SIGINT)中断信号被替换成sigcb函数 ctrl+(SIGQUIT)中断信号被忽略。
2.修改信号的整个处理动作
int sigaction(int signum,struct sigaction* new,struct sigaction* old);
//signum ---信号值
//new ---要修改后的信号动作
//old ---保存原来的信号动作
struct sigaction
{
void (*sa_handler)(int);//回调函数1
void (*sa_sigaction)(int, siginfo_t *, void *);//回调函数2
sigset_t sa_mask;//信号集合,在处理当前信号时,若到来其他信号,则阻塞这些信号
int sa_flags;
//设置为0,调用sa_handler回调函数操作;设置为SA_SIGINFO则调用sa_sigaction回调函数(不常用)
void (*sa_restorer)(void);
};
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
struct sigaction oldact;//保存原来的信号的处理动作
void sigcb(int signum)//回调函数
{
printf("recv signum:%dn", signum);
sigaction(signum, &oldact, NULL);//替换一次后,又换回默认的处理
}
int main (int argc, char *argv[])
{
struct sigaction newact;//要处理的信号动作
newact.sa_handler = sigcb;//调用回调函数sigcb
newact.sa_flags = 0;//使用sa_handler回调函数就要置0
sigemptyset(&newact.sa_mask);//先将信号集合清空,避免对当前信号处理产生影响
//sigaction修改信号的处理动作为newact,原来的动作使用oldact保存
sigaction(SIGINT, &newact, &oldact);
while(1)
{
printf("hellon");
sleep(1);
}
return 0;
}
因为回调函数中又将oldact动作置为空,newact置为原来保存在oldact中的信号动作。所以,ctrl+c这个信号只会被修改一次,往后又恢复了原来的信号动作。可以看出,这个函数比signal使用起来更加灵活,单也更加复杂。下面sigaction函数里边调用的结构体
5、自定义信号的捕捉流程
6、信号的阻塞
阻止一个信号的递达,信号依然可以注册,只是暂时不作处理。在pcb中有一个block位图(阻塞信号集合),凡是添加到这个集合中的信号,都表示需要阻塞,暂时不处理。
接口:
1.int sigprocmask(int how, sigset_t *set, sigset_t* old);
//对信号集合set进行how操作
//how---当前要对block进行的操作
//1.SIG_BLOCK:将set集合中的信号添加到block进程阻塞的信号集合中,block |= set,
// 表示阻塞set集合中的信号及原有的阻塞信号,并将原有的阻塞信号保存在old集合中(便于还原)。
//2.SIG_UNBLOCK:将set集合中的信号从block中移除,将set集合中的信号解除阻塞,block &=(~set);
//3.SIG_SETMASK:直接将block集合中的信号修改为set集合中的信号,block = set;
//set---要处理信号的集合
//old---保存处理信号原有的状态(便于还原)
2.int sigemptyset(sigset_t* set);
//清空set集合里面所有的信号
//成功返回0,失败返回-1
3.int sigfillset(sigset_t* set);
//将所有信号填充到set集合中
//成功返回0,失败返回-1
4.int sigaddset(int signum,sigset_t* set);
//将指定信号signum加入到set集合中
//成功返回0,失败返回-1
5.int sigdelset(sigset_t* set,int signum);
//将指定信号signum从set集合中移除
//成功返回0,失败返回-1
6.int sigismember(const sigset_t* set,int signum);
//实现: (1 << signum) & set
//判断信号signum是否在set集合中
//在返回1,不在返回0,出现错误返回-1
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void sigcb(int signum)
{
printf("signum = %dn",signum);
}
int main()
{
signal(2,sigcb);
signal(40,sigcb);
sigset_t set,old;//信号集合
sigemptyset(&set);//清空信号集合
sigemptyset(&old);
//sigaddset(int signum,sigset_t *set);将指定自信号添加到集合
sigfillset(&set);//将所有的信号添加到set集合中来
sigprocmask(SIG_BLOCK,&set,&old);//阻塞所有信号
printf("press enter to continuen");
getchar();//按下回车之前,程序卡在这里
sigprocmask(SIG_UNBLOCK,&set,NULL);//解除阻塞
return 0;
}
不可靠信号只注册了一次,可靠信号就处理了6次。
在Linux系统下,有两信号不可被阻塞,不可自定义修改处理方式,也不可能被忽略处理。 SIGKILL-9 和SIGSTOP-19
利用上述信号接口实现一个sleep函数,牛刀小试,点这里
7、函数可重入和不可重入
1、函数的重入:在多个执行流当中,进入同一个函数运行处理。
2、函数的可重入:函数重入后,不会造成数据的二义性和程序逻辑混乱,则这个函数就是可重入函数。
3、函数的不可重入:函数重入后,可能会造成数据的二义性和程序逻辑混乱,则这个函数就是不可重入函数。
函数的可重入和不可冲入的判定点:一个函数中是否对全局数据进行非原子操作(操作不可被打断)。
malloc 和free都是不可重入函数,在多个执行流当中操作需要注意。
举个栗子:
//loop.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int a = 0, b = 0;
void test() {
a++;
sleep(3);
b++;
printf("sum:%dn", a + b);
}
void sigcb(int signum)
{
test();
}
int main()
{
signal(SIGINT, sigcb);
test();
return 0;
}
由此可以看出,在main函数执行过程中,如果没有收到信号SIGINT,只执行test函数,这时就是一个正常状态。但是如果在test函数执行过程中,收到SIGINT信号,将会导致cpu去处理SIGINT信号所对应的事件sigcb,sigcb再调用函数test,将全局变量a++,b++,这样就会使main中的test函数中的处理结果偏离预期,出现上面的第二种结果。这个test函数就是不可重入函数。
8、SIGCHLD信号
子进程退出后,操作系统发送SIGCHLD信号给父进程,但是SIGCHLD信号的默认处理方式是忽略处理,因此在之前的程序中并没有感受到操作系统的通知,因此只能固定的使用进程等待来避免僵尸进程,但是在这个进程中父进程是一直阻塞的,只能一直等待子进程退出。
若在程序初始换阶段,将SIGCHLD信号的处理方式自定义,并且在自定义函数冲调用 waitpid ,这样的话子进程退出的时候,则会自动回调处理了,父进程就不需要一直等待了。
多个子进程退出时,都会想父进程发送SIGCHLD信号,但是SIGCHLD信号是非可靠信号,有可能会丢事件。例如:三个子进程同时退出,但是信号只注册了一次,意味着只会执行一次回调函数,调用一次waitpid,只能处理一个僵尸进程。
非可靠信号的丢失是不可避免的,因此只能再一次信号毁掉中处理完所有的僵尸进程。
while(waitpid(-1,NULL,WNOHANG) > 0);
//waitpid 返回值 > 0---退出子进程的pid ; ==0 ---没有子进程退出 ; < 0---出错
//循环是为了若有子进程退出则一直处理,直到没有子进程退出,则退出循环信号调用完毕
//WNOHANG ---将 waitpid设置为非阻塞,没有子进程退出的时候返回 0,退出循环,不要导致程序流程一直卡在回调函数中。
//zombie.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void sigcb(int signum)
{
while(waitpid(-1,NULL,WNOHANG)> 0)
{
printf("子程序退出n");
}
//返回值为子进程的pid,没有子进程退出,返回0,出错返回-1
}
int main()
{
signal(SIGCHLD,sigcb);//将信号SIGCHLD自定义为sigcb
pid_t pid = fork();
if(pid == 0)
{
sleep(5);//子进程退出,产生僵尸进程
exit(0);
}
if(fork() == 0)//再创建一个子进程,不退出
{
while(1)
{
printf("this is childn");
sleep(1);
}
}
while(1)
{
sleep(1);
printf("this is parentn");
}
return 0;
}
这样处理,就不会出现,子进程退出之前,父进程一直死等得现象。
最后
以上就是简单山水为你收集整理的进程通信(2)---信号的全部内容,希望文章能够帮你解决进程通信(2)---信号所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复