进程间通信
目的:每一个进程都有自己独立的虚拟地址空间和页表结构,这使得进程与进程之间相互独立,导致了进程和进程之间很难相互协作工作的问题,为了解决这种问题,进程与进程之间就需要沟通一下,也就是进程间通信。
进程间通信种类的细分:数据传输,数据共享,进程控制
管道
匿名管道
ps aux | // | 就是管道符
管道就是内核中的一块内存,相当于内核为进程间通信而创建的缓冲区
特性:
- 用于具有亲缘关系的进程。
- 管道是一个半双工,其数据流向只能是一个流向,也就是只能从写端流向读端,不可逆。
- 提供字节流服务,读端没有及时进行读取时,此时写端还在持续向管道中写入数据,那么后面写的数据就会追加在之前写的数据后面。数据其实都是二进制存储的,没有明确的数据边界,如果两次的数据表达的是不同的内容,就很有可能导致读端之后获取到数据时,不清楚管道中的数据是什么意思。(比如说,想向另一个进程输入一个 hello linux,但是写hello 时写端停止一下,这时又输入了一个china,那么等到写端正常写完之后,管道中的数据就是 hello china linux,跟原意不符)
- 读端读取数据的时候,是从管道中把所有的数据都拿走了,并不是从管道中的数据拷贝,而是"剪切",拿走之后管道中不再有数据。
- 生命周期随着进程同步的,进程结束后,管道美好的一生就结束了。
创建匿名管道
int pipe(int fd[2])
- fd[2]:是一个出参,也就是意味着我们在使用该函数的时候,需要传入一个int类型的大小为2的数组,在函数内部将数组的值进行填充,调用完成之后,程序员就能拿到管道对应的读写端
- fd[0]:读端,用户可以通过fd[0],当中保存的文件描述符来对管道的内存进行读操作
- fd[1]:写端,用户可以通过fd[1],当中保存的文件描述符来对管道的内存进行写操作
- 返回值:成功返回0,失败则返回-1
匿名管道的特性
- PIPE_SIZE:64k,匿名管道的大小
- PIPE_BUF:4096,保证写入数据或读取数据的原子性,即当前操作不能被打断,运行的结果只能有两个,要么是0(操作没有完成),要么是1(操作完成),不会存在只完成一半的情况。
文件描述符阻塞的情况下
管道符没有数据,调用read会进行阻塞,等待写端进行
管道符被写满的时候,调用write会进行阻塞,等待读端进行读
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe error!");
return 0;
}
//创建一个子进程
ret = fork();
if(ret < 0)
{
perror("fork error!");
return 0;
}
else if(ret == 0)
{
//child 读
// sleep(5);
char buf[1024] = {'\0'};
//read(fd[0],buf,sizeof(buf) - 1);
//printf("i am child i read buf = [%s]\n",buf);
//1.管道中没有数据,read 就会阻塞
//2.管道中的数据被拿走了
read(fd[0],buf,sizeof(buf) - 1);
while(1)
{
sleep(1);
}
}
//father 写
// sleep(60);
// write(fd[1],"hello linux",11);
//查看写多少字节可以写满
int count = 0;
while(1)
{
write(fd[1],"6",1);
//int status = write(fd[1],"6",1);
//if(status != 1)
//{
//printf("write size status = [%d]\n",status);
//break;
//}
count++;
printf("count = [%d]\n",count);
}
return 0;
}
文件描述符属性的设置
int fcntl(int fd, int cmd, ... /* arg */ );
- fd:文件描述符
- cmd:命令,即想让fcntl函数实现的操作
- F_GETFL:获取当前文件描述符的属性
- F_SETFL:设置当前文件描述符的属性
- 返回值:文件描述符的属性会通过该函数以返回值的形式返回,在使用的时候以位图的方式使用
非阻塞状态的属性:O_NONBLOCK
返回值是 flags |= O_NONBLOCK
//获取当前文件描述符的属性,并通过返回值返回
int flags = fcntl(fd[1],F_GETFL,0);
flags |= O_NONBLOCK;
当创建出来的匿名管道,更改对应读写端文件描述符为非阻塞状态时:
- 不进行读,一直去写:设置写端为非阻塞,读端可以不用关心,因为没有用到读端。
- 读端不关闭,调用写。由于在管道中能写入的数据有限,等到管道中的数据写满的时候,写入就会失败,write返回-1,报错当前资源不可用。
- 当所有的读端全部关闭,调用写。会造成写端的进程收到SIGPIPE信号,当前写端程序会被杀死,管道会“破裂”。
- 不写,进行读:设置读端为非阻塞,写端可以不用关心,因为没有用到写端
- 写端不关闭,读端进行读。read调用后会返回-1,报错资源不可用。
- 当所有的写端关闭,进行读。read是正常调用的,read会返回读到的字节数量,即0。
当操作管道的数据小于PIPE_BUF时,管道保证进程在读取和写入数据的时候的原子性。
数据的原子性
原子性:当前的操作不能被打断,也就是当前的操作结果只有0(未完成)或者1(完成)。
关于原子性,可以这样理解,假设有图中的三个进程,两个进程向管道中写数据进行通信,C进程获取管道中的数据进行处理。当A进程向管道中输入部分字节的时候,突然因为"特殊原因"停止了输入(此时程序计数器和上下文信息都会保存A进程退出前的状态),这个时候管道就空着了,B进程可以趁机输入数据,当B进程输入完成时A进程又正常了,然后继续写入刚才没有写入完全的字节,那么在A进程输入完全之后,C进程所拿到的数据并不是A进程想要写入的数据,因为中间还夹杂了B进程的数据。
这个时候,为了保证数据的准确,那么在一个进程向管道中写入数据的时候,其他进程不可以打扰,这样就可以保证写入数据的原子性。
更加具体的理解原子性,就用我们常见的自加++命令来说吧
首先,有两个自加的命令,内存中自加对象的数据是10,因为这是后置++,在执行的时候,先使用后+1,如果进程不具有原子性,那么在A进程取到内存中的数据后被打断,这使程序计数器和上下文信息一起保存A进程的操作,这个时候因为CPU空闲下来,那么B进程就可以执行了,此时内存中的数据还是10,在B进程执行完毕之后,就会改变val数据的值为11,B进程执行完毕,A进程继续中断前的情况进行执行,此时A进程会执行+1的操作,返回的结果还是11。
这样当两个进程都执行完毕,内存中的数据是11,但是根据我们的想法,两次自加操作所返回的结果应该是12,所以说,进程之间在执行的时候,不可以被打断,不然会破坏原有的结果,即原子性。
临界资源
在原子性中所操作的数据被称为临界资源,同一时间,当前的进程只能被一个进程所访问。由于不同的进程对资源进行访问时(读,写,修改),可能会造成数据的二义性,那么计算机是如何保证对临界资源访问的时候,不会造成数据的二义性呢。
- 互斥:同一时间保证只能有一个进程访问临界资源
- 同步:保证对临界资源访问的合理性(当读端不读时,写端一直写,在数据写满管道时候,就不会继续再向管道中写)。
命名管道
命名管道是具有标识符的管道,内核当中开辟的内存是有标识的,不同的进程可以通过名字访问带命名管道。
- 命名管道的创建
命令行的方式
mkfifo [命名管道的名称]
管道文件的文件类型就是p
调用函数的方式
mkfifo(char* pathname,mode_t mode)
- mode:指定创建出来的命名管道的读写权限
- pathname:需要创建的命名管道的路径
- 用户可以通过操作命名管道文件来对内核中的命名管道的内存区域进行读写操作
特性:
- 具有标识符,可以满足不同进程之间相互通信。同时开通两个shell界面,一个文件负责写,一个文件负责读,写的文件写入后,另一个进程中读取的文件可以直接读取。
- 生命周期是跟随进程的
- 命名管道的其他特性和匿名管道相同
使用命名管道来完成文件的拷贝
- 读取文件,写入命名管道:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0);
int main(int argc,char* argv[])
{
mkfifo("cfifo",0644);
int incfifo = open("abc",O_RDONLY | O_CREAT);
if(incfifo == -1)
{
ERR_EXIT("open file");
}
int outcfifo = open("cfifo",O_WRONLY);
if(outcfifo == -1)
{
ERR_EXIT("open fifo");
}
char buf[1024] = {'\0'};
int n;
while((n = read(incfifo,buf,1024)) > 0)
{
write(outcfifo,buf,n);
}
close(incfifo);
close(outcfifo);
return 0;
}
- 读取管道中数据,写入目标文件:
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(int argc,char* argv[])
{
int outfifo = open("abc.bak",O_WRONLY | O_CREAT | O_TRUNC,0644);
if(outfifo == -1)
ERR_EXIT("open file");
int infifo = open("cfifo",O_RDONLY);
if(infifo == -1)
ERR_EXIT("open fifo");
char buf[1024] = {'\0'};
int n;
while((n = read(infifo,buf,1024)) > 0)
{
write(outfifo,buf,n);
}
close(infifo);
close(outfifo);
unlink("cfifo");
return 0;
}
命名管道模拟实现server&client通信
- server.c 从命名管道中读取数据
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
if(mkfifo("cfifo",0644) < 0)
ERR_EXIT("mkfifo error");
int rfd = open("cfifo",O_RDONLY);
if(rfd < 0)
ERR_EXIT("open cfifo error");
char buf[1024] = {'\0'};
while(1)
{
buf[0] = 0;
printf("please wait ....\n");
ssize_t s = read(rfd,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;
printf("client say# %s\n",buf);
}
else if(s == 0)
{
printf("client quit,exit now!\n");
exit(EXIT_SUCCESS);
}
else
{
ERR_EXIT("read error");
}
}
close(rfd);
return 0;
}
- client.c 向命名管道中写数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd = open("cfifo",O_WRONLY);
if(wfd < 0)
ERR_EXIT("open cfifo");
char buf[1024] = {'\0'};
while(1)
{
buf[0] = 0;
printf("please enter# ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
write(wfd,buf,strlen(buf));
}
else if(s <= 0)
{
ERR_EXIT("read error");
}
}
close(wfd);
return 0;
}
现象:写的进程向管道中写入数据后,读取进程立刻会接收到信号,完成通信。
system v共享内存
- 原理:
创建共享内存的时候,先在物理内存当中开辟一段空间,各个进程通过页表结构将物理内存映射到自己的虚拟地址空间当中的共享区各个进程之间通过修改自己虚拟地址空间中的共享区的地址来完成通信功能。
特性:
- 共享内存使最快的进程间通信的方式
- 共享内存是不带有同步和互斥功能的
- 写入数据是按照覆盖的方式进行
- 不同的进程对共享内存区域进行读的时候,并不会抹除物理内存中的值
创建共享内存 & 使用共享内存的接口
创建共享内存:
int shmget(key_t key, size_t size, int shmflg);
- key:共享内存的标识符
- size:共享内存的大小
- shmflag:
- IPC_CREAT:如果想获取的共享内存不存在,则创建共享内存。如果存在则返回共享内存的操作句柄(返回值)。
- IPC_EXCL | IPC_CREAT:如果想获取的内存存在,则报错
- 按位或上权限,可以使用8进制的数字来进行参数的按位或
- 返回值:成功返回共享内存的操作句柄
将进程附加到共享内存上
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:共享内存的操作句柄
- shmaddr:程序员去指定映射到共享区的哪一个地址,一般不去选择,设置成NULL,然后由操作系统指定将内存映射到哪一个地址上。
- shmflg:
0(零):可读可写
IPC_RDONLY:只读 - 返回值:返回映射到共享区的地址,我们就可以操作这个地址来对物理内存区进行读写。
从共享内存中分离进程
int shmdt(const void *shmaddr);
- shmaddr:共享区当中映射的物理内存的首地址,shmat的返回值
函数成功返回0,失败返回-1
共享内存的销毁
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:共享内存的操作句柄
- cmd
销毁 IPC_RMID:删除共享内存,标记共享内存为删除状态
获取共享内存信息 IPC_STAT:获取共享内存状态,需要搭配 struct shmid_ds - buf:需要作为一个出参,获取共享内存状态的信息,传入一个struct shmid_ds结构体对象的地址
共享内存的生命周期
- 共享内存的生命周期是跟随操作系统内核的
共享内存的查看
ipcs
ipcs -m
ipcrm -m [shmid] //可以删除共享内存
如果删除了一个有进程附加的共享内存,操作系统的做法是:
- 先标记当前的共享内存为destroy状态,并且将key设置为0x00000000,表示当前的共享内存不能再被其他进程所附加,同时会释放共享内存
- 会导致正在附加到该共享内存上的进程有崩溃的风险,一般不要删除有进程附加的共享内存。
当进程退出的时候,操作系统就会将描述共享内存的结构体也释放掉
读取共享内存中的数据
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define shm_key 0x9999999
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int shmid = shmget(shm_key,1024,IPC_CREAT | 0664);
if(shmid < 0)
ERR_EXIT("shmget error");
struct shmid_ds buf;
shmctl(shmid,IPC_STAT,&buf);
printf("shmsize: [%ld]\n",buf.shm_segsz);
void* lp = shmat(shmid,NULL,0);
if(!lp)
ERR_EXIT("shmat error");
//printf("readshm read [%s]\n",(char*)lp);
while(1)
{
printf("readshm read [%s]\n",(char*)lp);
sleep(1);
}
shmdt(lp);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
向共享内存中写入数据
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define shm_key 0x9999999
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int shmid = shmget(shm_key,1024,IPC_CREAT | 0664);
if(shmid < 0)
ERR_EXIT("shmget error");
void* lp = shmat(shmid,NULL,0);
if(!lp)
ERR_EXIT("shmat error");
//sprintf((char*)lp,"%s-%d","hello linux",66);
int i = 0;
while(1)
{
sprintf((char*)lp,"%s-%d","hello linux",i);
sleep(1);
i++;
}
//shmdt(lp);
//shmctl(shmid,IPC_RMID,NULL);
}
可以证明上面的结论,共享内存在被读取的时候不会清理掉上一次的数据,但是在写入的时候会清理共享内存中的数据。
消息队列
他满足队列的特性,就像先进先出,在底层中他是以链表的形式实现的,在内核当中创建。
- 队列中每一个元素都有自己的类型,类型之间有一个优先级的概念
- 消息队列的生命周期跟随内核
- 消息队列可以进行双工通信,克服了管道的无格式字节流的缺点
- 每一个结点最大消息的最大发送字节数 8K
- 队列中所有消息的长度之和 16384
- 系统当中最大的队列数 2379
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- key:消息队列的标识符
- msgflg:
IPC_CRETA
IPC_CRETA | IPC_EXCL
可以按位或上权限
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
- msgsnd:
- msgid:消息队列的操作句柄
- msgp:发送的数据
- msgsz:数据的长度
- msgflg:
0(零):当队列满了就会阻塞
IPC_NOWAIT:如果说队列满了,则当前发送的操作不会进行阻塞,函数返回。
- msgrcv:
- msgtyp:
0(零):表示什么类型都可以接收
msgtype > 0:则返回消息队列中消息类型为msgtype的第一个消息
msgtype < 0:则返回队列中消息类型小于等于msgtype的绝对值的消息,如果这样的消息比较多,则返回类型最小的那个消息。
- msgtyp:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- cmd:
IPC_STAT
IPC_SET
IPC_RMID