进程之间是独立的,但是进程之间还是要相互协作的,这种协作叫通信。
本片博文非常的长~~~请按需求阅读
目录
-
1》》目的
数据传输:一个进程的数据发送给另一个进程
数据共享:多个进程共享同样的进程
通知事件:一个进程需要通知另一个进程发生了某种事件
进程控制:一个进程完全控制 另一个进程的执行
-
2》》通信方式
由于进程的独立性,通信需要双方拥有公共的媒介才能通信,而媒介由操作系统提供;由于通信的场景不同或者目的不同,操作系统提供了不同的进程间通信方式。
a、管道
b、共享内存
c、消息队列
d、信号量
ipcs 查看进程间通信方式(-m共享内存 -s信号量 -q消息队列)
ipcrm 删除通信方式
-
3》》管道
-
概念:
一个进程连接到另一个进程的数据流,称之为管道。这便可以达成一个目的——数据传输 - 原理:
操作系统在内核创建一块缓冲区,缓冲区便就是管道,并为用户提供管道的操作句柄----->传输的是字节流
一共有两个句柄:fd[2]—— ( fd[0] 和 fd[1] ) fd[0]用于读取,fd[1]用于写入
- 特性:
1>若管道中没有数据,read读取数据,会发生阻塞,直到读到数据返回。
2>若管道中数据满了,如果继续写入数据,则会发生阻塞,直到能够写入数据或者写入完成。
3>若所有读端关闭,继续写入数据会触发异常。SIGPIPE
4>若所有写端关闭,进程会将管道数据全部读取完后退出程序。
5>声明周期随进程,自带同步和互斥
(同步:对临界资源访问的时序可控 互斥:对资源的访问唯一)
- 命名管道/匿名管道 ——都是半双工通信
- 匿名管道:
没有名字的缓冲区,因此在内核中,如果是两个独立的进程,即使其中一个进程创建了管道,另一个进程是找不到它的句柄的,所以匿名管道只能是用于具有亲缘关系的进程进行通信(fork()创建子进程,父子进程同时指向匿名管道)
int pipe(int fd[2])————成功返回0,失败返回错误代码
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5
6 int main()
7 {
8 int pipefd[2];
9 int ret = pipe(pipefd);
10
11 if(ret < 0)
12 {
13 printf("PIPE ERROR\n");
14 return -1;
15 }
16
17 int pid = fork();
18 if(pid < 0)
19 {
20 printf("FORK ERROR\n");
21 exit(-1);
22 }
23 else if(pid == 0)//子进程
24 {
25 char buff[1024];
26 read(pipefd[0],buff,1023);
27 printf("child readed:%s\n",buff);
28 }
29 else
30 {
31 char buff[1024] = {0};
32 scanf("%s",buff);
33 printf("father write:%s\n",buff);
34 write(pipefd[1],buff,strlen(buff));
35 }
36 wait(NULL);
37 return 0;
38 }
- 命名管道:
文件系统的可见性,有文件可见于文件系统之中。
创建一个带名字的缓冲区,操作和匿名管道相同。
open打开管道文件的特性:
若管道文件以只读方式打开,若没有被其他进程以写的方式打开,则被阻塞,直到被其他进程以写的方式打开这个管道文件
若管道文件以只写方式打开,若没有被其他进程以读的方式打开,则被阻塞,直到被其他进程以读的方式打开这个管道文件
管道的实例:管道符的实现 |
主进程创建两个子进程,在两个中分别进行程序替换让两个子进程分别运行ls程序和grep程序
ls 将结果写入到标准输出
grep从标准输入读取数据
-
4》》共享内存
进程间通信最快的一种
- 概念:在物理内存中开辟一块空间,并将空间映射到各个进程的虚拟地址空间的共享区,其他进程可以通过虚拟地址直接对内存进行操作。
- 共享内存的操作步骤:(shared memory------------>shm)
a、创建共享内存——指定标识符,大小,权限,并返回一个句柄 ---->shmget
b、将共享内存映射到虚拟地址空间——对句柄操作-------------------->shmat
c、对共享内存进行任意操作
d、解除映射关系----------------------------------------------------------->shmdt
e、删除共享内存----------------------------------------------------------->shmctl
int shmget(size_t key, size_t size, int shmflg)
key:共享内存的标识符/名称
size:共享内存的大小
shmflg:与mode_t权限类似
成功返回shmid,失败返回-1
void* shmat(int shmid, const void* shmaddr, int shmflg)
shmid:共享内存标识符
shmaddr:指定连接的地址
shmflg:SHM_RND\SHM_RDONLY
成功返回共享内存首地址 失败返回 -1
int shmdt(const void *shmaddr);
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
#define SHM_SIZE 4096
int main()
{ //创建共享内存
int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
if(shmid < 0)
{
perror("CREATE ERROR\n");
return -1;
}
//映射首地址
char* address = (char*)shmat(shmid,NULL,0);
//映射首地址
char* address = (char*)shmat(shmid,NULL,0);
if(address ==(void*)-1)
{
perror("CONECT ERROR\n");
return -1;
}
//进行内存操作
int i = 0;
while(1)
{
sprintf(address,"------%d",++i);sleep(1);
}
//接触映射关系
shmdt(address);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
#define SHM_SIZE 4096
int main()
{ //创建共享内存
int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
if(shmid < 0)
{
perror("CREATE ERROR\n");
return -1;
}
//映射首地址
char* address = (char*)shmat(shmid,NULL,0);
//映射首地址
char* address = (char*)shmat(shmid,NULL,0);
if(address ==(void*)-1)
{
perror("CONECT ERROR\n");
return -1;
}
//进行内存操作
int i = 0;
while(1)
{
printf("收到:%s\n",address);
sleep(1);
}
//接触映射关系
shmdt(address);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
第一个程序运行之后,运行第二个程序,便会得到下面的图1。如果第一个程序停止,第二个的数据不会变化,图2。第二个程序停止在运行,数据不会从停止的时候开始,因为共享区的数据一直在被改变图3。
ipcs -m
ipcrm:删除该id之后,我并没有结束我的进程,所以并不会真正的删除我们的共享内存,当我们后面的链接数nattch为0的时候,就真正的删除了
-
5》》进程信号
信号:就像我们听到下课铃声,铃声响起(信号产生)--->我们听到,并且识别(信号注册)--->知道下课了,忘记这个提示(信号的注销)---->开始玩耍(信号的处理)/拖堂(信号的阻塞)
作用:事件通知——实际是软中断(软件中断)
-
1、Linux信号的种类:
查看:kill -l (1~31 非可靠信号,继承Unix 34~64 可靠信号(实时信号))
-
2、信号的产生:
就像下课铃声打了~~~~。信号产生。
- 软件方式:
函数:
int kill(pid_t pid, int sig):指定进程,指定信号
int raise(int sig):给调用进程指定信号
int abort(void):给调用进程发送SIGBART信号
size_t alarm(size_t second):second秒后发送SIGALRM信号
我需要每次睡眠一秒,不然不知道打印会很多很多~
总的代码。
- 硬件方式:
ctrl + c ctrl + z(只是停止进程,并未退出) ctrl + l
(信号的到来会打断当前可中断睡眠状态)
core dumped———核心转储:异常退出时保存程序运行信息--->默认关闭
ulimit -a 查看转储文件大小
ulimit -c 设置转储文件大小-----》只有设置了大小,才会生成core文件
core文件命名方式:core.pid
core文件的使用:
gdb 可执行文件 ---> core-file core.pid
-
3、信号的注册:
下课铃声进入我们和老师的大脑。
pcb中有 struct sigpending,该结构体中有sigset_t(信号集合——位图0\1)
操作系统给一个进程发送信号,实际就是向进程的信号pending集合中添加信号(修改位图)
位图只能标记信号是否存在,不能标记到来的信号的个数,这就牵出可靠信号和不可靠信号。
pcb中有sigqueue链表,信号到来会组织一个结点添加到链表中
可靠信号到来:修改位图,每个信号都组织结点添加到链表中
非可靠信号到来:修改位图,若位图已经为一,修改位则什么也不做,否则添加一个结点。
-
4、信号的注销:
铃声响完之后,铃声不会一直在脑海里
可靠信号:可能结点有多个,所以删除结点后判断是否有多个相同结点,判断是否修改位图
非可靠信号:结点只有一个,删除结点位图修改为0
-
5、信号的处理:
准备下课~~~~
当我们收到接收到一个信号,就需要来处理这个信号,也就是处理每个信号对应的操作。每个信号都有其默认的操作,但是操作系统也提供了修改其默认操作的接口。
- 1) signal(int signum, sighander_t handler)
这是给信号修改一个处理函数,这个也可以当成是信号的注册,因为它就是给信号赋一个操作。当出现signum号信号,开始执行这个handler操作。
signum:信号编号
handler:操作,操作有一下三种:
默认:每个结点对应了一个事件,每个事件在操作系统都有定义完毕的处理方式 SIG_DFL
忽略:忽略信号 SIG_IGN
自定义:用户自定义函数(回调函数),替换内核中的处理方式
我定义一个处理的函数,每当我发起SIGINT信号,就会去调我们的fun函数。
- int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;//通常为 0,调用第一个函数sa_handler,如果是SA_SIGINFO就调用第二个函数
void (*sa_restorer)(void);
};
sigaction函数可以读取和修改与指定信号相关联的处理动作。signal(int signum, sighander_t handler)内部也是调用的这个函数。
参数:igno是指定信号的编号。
若act指针⾮空,则根据act修改该信号的处理动作。
若oact指针⾮空,则通 过oact传出该信号原来的处理动作。
void fun(int num)//自定义操作
{
printf("action !! %d\n",num);
}
int main()
{
struct sigaction act;
struct sigaction old;
sigemptyset(&act.sa_mask);
act.sa_handler = fun;//自定义操作
act.sa_flags = 0;
sigaction(2,&act,&old);//不可靠信号
sigaction(SIGRTMIN+5,&act,&old);//可靠信号
//下面是信号的阻塞,在下下一小节/
sigset_t newset;
sigset_t oldset;
sigemptyset(&newset);
sigfillset(&newset);
sigprocmask(SIG_BLOCK,&newset,&oldset);//阻塞所有信号
printf("bolck………………………………\n");
getchar();
sigprocmask(SIG_UNBLOCK,&newset,NULL);
}
2号信号触发了4次,39号信号也调用了4次,但是不可靠信号之处理了一次。说明改信号只注册了一次,这就验证了前面两种信号的注册
- 信号的捕捉
处理操作是我们用户自定义操作的时候,进程会从用户态和内核态相互切换。
如果信号的处理动作是⽤户⾃定义函数,在信号递达时就调用这个函数,这称为捕捉信号
进程从用户态到内核态三种形式:系统调用接口,程序异常,中断
-
6、信号的阻塞:
就是说信号来了,但是暂时不处理这个信号,阻止信号的递达。就像下课铃声响了,老师拖堂,阻塞下课。
(注意: 9信号和19信号不能被阻塞和自定义)
- 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, in t signo);
- int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1
SIG_BLOCK : 阻塞set集合中的信号,将原有阻塞放入oset
SIG_UNBLOCK: 对set集合中的信号解除阻塞
SIG_SETMASK: 将set集合中的信号添加到阻塞集合中
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是⾮空指针,则 更改进程的信号屏蔽字,参数how指⽰如何更改。
如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
//如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到 oset⾥,然后根据set和how参数更改信号屏蔽字
/*实现的过程:
*1、定义一个集合
*2、向集合哄添加要阻塞的信号
*3、阻塞这个集合中的所有信号
*4、获取换行getchar()
*5、对集合中的信号进行解除阻塞
*/
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main()
{
sigset_t newset;
sigset_t oldset;
sigemptyset(&newset);
sigfillset(&newset);
sigprocmask(SIG_BLOCK,&newset,&oldset);
printf("bolck………………………………\n");
getchar();
sigprocmask(SIG_UNBLOCK,&newset,NULL);
return 0;
}
//如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则 更改进程 的信号屏蔽字,参数how指⽰如何更改。。
void print(sigset_t* p)
{
int i = 1;//信号的集合是从1开始的
for(i; i < 32; ++i)
{
if(sigismember(p,i))//判断指定信号是否在目标信号集中
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
int main()
{
sigset_t newset,oldset,pending;
sigemptyset(&newset);
sigfillset(&newset);
sigprocmask(SIG_BLOCK,&newset,NULL);
raise(5);//给调用进程发送5号信号
raise(6);//给调用进程发送6号信号
raise(7);//给调用进程发送7号信号
raise(8);//给调用进程发送8号信号
getchar();
sigpending(&pending);
print(&pending);//打印我们的未决集合
sigprocmask(SIG_UNBLOCK,&newset,&oldset);//解除阻塞
return 0;
}
//由于阻塞了所有信号,所以2号和3号也阻塞着。未决集合,注册了,但是没有处理的信号。
所以23 5678号信号都打印了
-
6》》可重入函数和不可重入函数
:重入,我们可以理解为多个执行流可以进入。函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数, 这称为重⼊。那么这样我们得考虑数据安全的问题。所以能否在多个时序操作中对全局数据能时序性的访问,也就是全局数据是正常的改变就是重入,反之就是不可重入。看下面的例子
//C语言
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int a = 1;
int b = 1;
int test()
{
++a;
sleep(4);
++b;
return a+b;
}
void sigcb(int num)
{
printf("signal-----> %d\n",test());
}
int main()
{
signal(SIGINT,sigcb);//CTRL + C 触发
printf("main-----> %d\n",test());
return 0;
}
//如果我们的主控流程先执行,即信号后执行,我们预期结果是不是应该是
signal----->6
main----->4
但是………………4秒类按CTRL + C,不然触发不了信号。
为什么和预期结果不一样呢?
这就是因为这个test是不可重入的函数,也就是说如果有其他执行流进入,会造成数据错误,造成数据不安全问题。在4秒类,a先++、a==2,等四秒,这时信号进来,a++、a==3、,b++,b==2,然后主控流程的printf最后执行的时候,b++、b==3,得到上面的结果。
-
7》》volatile关键字
保持内存可见性,防止编译器对代码过度优化。看下面的例子
//C语言
#include<stdio.h>
#include<signal.h>
int a = 1;
void sigcb(int num)
{
printf("signal-----》%d\n",num);//CTRL+C,触发这个操作,改变a的值
a = 0;
}
int main()
{
signal(SIGINT,sigcb);
while(a)//值改变之后,while退出循环
{ }
return 0;
}
//真的是这样的吗??
有没有发现我编译的时候加了-O2,这就是让编译器对代码进行优化。对于上面的代码,如果a这个数据用的频率偏高,而且值一直没有变化,所以它就将a的值放入寄存器中,那么CPU在寄存器中拿数据就不去内存中了,但是我们改变的是内存中的a的值,所以a的值并没有改变。这时我们对 a 加个关键字volatile。
//C语言
//C语言
#include<stdio.h>
#include<signal.h>
volatile int a = 1;
void sigcb(int num)
{
printf("signal-----》%d\n",num);//CTRL+C,触发这个操作,改变a的值
a = 0;
}
int main()
{
signal(SIGINT,sigcb);
while(a)//值改变之后,while退出循环
{ }
return 0;
}
//真的是这样的吗??
这时一下就退出了,加这个关键字的时候,编译器就不会把a加入到寄存器中。