进程通信的目的有四点:
管道
System V IPV
POSIX IPC
但是这篇博客是讲管道、System V共享内存。
在学Linux指令的时候,知道管道是|
来表示的。
如一行指令head -n5 text.txt | tail -n3
表示获取text.txt文件3~5行内容。
head命令通过选项 -n5 把text.txt文件中的前五行内容通过管道传输给tail进程把传输过来的内容中的末尾三行内容拿出来。这里管道就提供了传输数据的作用。
如图(简易图)
head进程是通过管道来对tail进程进行数据传输,这看似很正常。但这个管道是谁提供的呢?进程不是具有独立性吗?这两个进程是怎么玩到一块去的呢?
进程和进程具有独立性没错,但是不一定玩不到一块去。比如,人是独立的,但是人可以和人交流,只是要通过一个媒介。而OS就可以提供一个媒介——管道。
OS提供一段内存区域,让head进程和tail进程都看到这块区域,让这两个进程进行通信。所以,进程与进程之间进行通信的本质是看到同一份资源,这种资源称之为临界资源(内存、文件内核缓存等)。
匿名管道:创建一种无名的管道,提供具有血缘关系的进程进行通信。
函数
#include
int pipe(int fd[2])
参数:fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2];
//创建匿名管道
if(pipe(fd)<0){
perror("pipe!");
return 1;
}
//创建子进程
int id=fork();
if(id==0){
//child
close(fd[0]);
const char* mag="hello linux\n";
int count=5;
while(count){
write(fd[1],mag,strlen(mag));
count--;
sleep(1);
}
exit(0);
}
//father
close(fd[1]);
char buff[64];
while(1){
ssize_t s=read(fd[0],buff,sizeof(buff));
if(s>0){
buff[s]='\0';
printf("child --->father#%s",buff);
sleep(1);
}
else if(s==0){
printf("read end\n");
break;
}
else{
printf("error\n");
break;
}
}
//等待子进程结束,回收资源,防止僵尸进程。
waitpid(id,NULL,0);
close(fd[0]);
return 0;
}
运行结果:
child --->father#hello linux
child --->father#hello linux
child --->father#hello linux
child --->father#hello linux
child --->father#hello linux
read end
<1> 如果说通信可以通过文件来作为媒介,那么为什么不直接open一个文件来呢?要用pipe来创建管道?
答:pipe创建的文件是内存文件,数据一定不会刷新到磁盘。并且用普通文件会有很多问题(同步与互斥),有IO参与会降低效率,没有必要。
<2> 创建子进程进行写入难道不会发生写时拷贝吗?
答:不会,管道是OS提供的,子进程写入时,不会改变父进程的数据区,故不会发生写时拷贝。
<3> 子进程还没写完,父进程就不会读取吗?
答:管道是自带同步与互斥的。不会发生子进程还没写完,父进程就开始读了。
父进程创建了匿名管道,父进程PCB中的files*指针指向file_struct,通过文件描述符找到file结构体,对管道进行读写。
fork子进程,是为了让子进程也可以看到相同的管道,对管道进行读写,这样就可以通信了。
管道只能单向通信,只能一方读,另一方写。所以在fork之后,要关闭掉不需要的描述符。
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件的思想”。
当没有数据可读时:
当管道满了时:
如果管道写端对应的文件描述符被关闭,read返回0
如果父进程write关闭时,子进程read没有意义,子进程会接收到13号信号退出。
用代码演示子进程接收信号退出:
#include
#include
#include
#include
#include
int main()
{
int fd[2];
if(pipe(fd)<0){
perror("pipe!");
return 1;
}
pid_t id=fork();
if(id==0){
//child
close(fd[0]);
const char* mag="hello linux\n";
write(fd[1],mag,strlen(mag));
exit(0);
}
//father
//关闭读端和写端
close(fd[1]);
close(fd[0]);
int status=0;
waitpid(id,&status,0);
//打印信号
printf("child get signal:%d\n",status&0x7F);
return 0;
}
运行结果:
child get signal:13
当要写入的数据量不大于PIPE_BUF时,Linux将保持原子性。
当要写入的数据量大于PIPE_BUF时,Linux将不保持原子性。
(翻译过来的) POSIX.1-2001规定,小于PIPE_BUF字节的写入(2)必须是原子的:输出数据作为连续序列写入管道。写入超过PIPE_BUF字节可能是非原子的:内核可能会将数据与其他进程写入的数据交错。POSIX.1-2001要求管道长度至少为512字节。(在Linux上,PIPE_BUF是4096精确的语义取决于文件描述符是否为非阻塞(O_NON?块),管道是否有多个写入程序,以及在n上,要写入的字节数
管道有多大?代码测试一下。
#include
#include
int main()
{
int fd[2];
pipe(fd);
pid_t id=fork();
if(id==0){
close(fd[0]);
char a='a';
int count=0;
while(1){
write(fd[1],&a,1);
count++;
printf("%d:a\n",count);
}
}
sleep(1000);
return 0;
}
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
匿名管道对具有血缘关系的进程进行通信,那么两个毫不相关的进程是如何通信的呢?
答:进程的通信本质是看到同一份资源,毫不相干的进程可以通过命名管道进行通信。
命名管道可以直接在命令行上进行创建。
mkfifo filename
//server.c ->写
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("filename",O_WRONLY);
char *mag="ni hao a\n";
write(fd,mag,strlen(mag));
close(fd);
return 0;
}
//client.c ->读
#include
#include
#include
#include
#include
#include
int main()
{
char buff[64];
int fd=open("filename",O_RDONLY);
ssize_t s=read(fd,buff,sizeof(buff));
buff[s]='\0';
printf("srever----->client:%s",buff);
close(fd);
return 0;
}
运行结果
srever----->client:ni hao a
命名管道也可以从程序里创建,相关函数有:
#include
#include
int mkfifo(const char *filename,mode_t mode);
第一个参数是文件名或者路径。第二个参数是文件的权限。
成功是返回0,失败是返回-1。
原理:
把一个普通文件的内容通过server1进程读取到管道中,再通过client1进程创建新的一个文件,并把管道中的内容写入新文件中,这样就完成有文件的copy.
//server1.c 读取普通文件内容——>把内容写入管道中。
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd1=open("copyfile",O_RDONLY);//以读的方式打开
if(mkfifo("filename",0666)<0){
perror("mkfifo!");
return 1;
}
int fd=open("filename",O_WRONLY);//创建管道
char mag[128];
while(1){
mag[0]=0;
ssize_t s=read(fd1,mag,sizeof(mag));//读取普通文件的数据
if(s>0){
mag[s]=0;
write(fd,mag,strlen(mag));//把数据写入管道中
}
else{
break;
}
}
return 0;
}
//client1 读取管道内容,并把读取到的内容写入到新创建的文件中。
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd1=open("copy",O_WRONLY|O_CREAT,0666);//以写的方式创建一个新文件
int fd=open("filename",O_RDONLY);//以读的方式打开管道
char buff[128];
while(1){
buff[0]=0;
ssize_t s=read(fd,buff,sizeof(buff));//读取管道的数据
if(s>0){
write(fd1,buff,strlen(buff));//把读到的数据写入到新创建的文件中
}
else{
break;
}
}
return 0;
}
//server 写端
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
if(mkfifo("filename",0666)<0){//创建命名管道
perror("mkfifo!");
return 1;
}
int fd=open("filename",O_WRONLY);//只以写的方式打开命名管道
if(fd<0){
perror("open!");
return 1;
}
char mag[128];
while(1){
mag[0]=0;
printf("server say $:");//提示发信息
ffshul(stdout);//刷新缓冲区
ssize_t s=read(0,mag,sizeof(mag));//从键盘中获取内容填入mag
if(s>0){
mag[s-1]=0;
write(fd,mag,strlen(mag));//把mag的内容写入命名管道
}
else{
printf("error\n");
break;
}
}
return 0;
}
//client读端
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("filename",O_RDONLY);
if(fd<0){
perror("open!");
return 1;
}
char buff[128];
while(1){
buff[0]=0;
ssize_t s=read(fd,buff,sizeof(buff));
if(s>0){
buff[s]=0;
printf("server-->client#:%s\n",buff);
}
}
return 0;
}
//server.c
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
if(mkfifo("filename",0666)<0){
perror("mkfifo!");
return 1;
}
int fd=open("filename",O_WRONLY);
char mag[128];
while(1){
mag[0]=0;
printf("server say $:");
fflush(stdout);
ssize_t s=read(0,mag,sizeof(mag));
if(s>0){
mag[s-1]=0;
write(fd,mag,strlen(mag));
}
else{
printf("error\n");
break;
}
}
return 0;
}
//client.c
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("filename",O_RDONLY);
char buff[64];
while(1){
buff[0]=0;
ssize_t s=read(fd,buff,sizeof(buff));
if(s>0){
if(fork()==0){ //创建子进程来完成任务
execlp(buff,buff,NULL);//进程替换
exit(0);
}
waitpid(-1,NULL,0);//阻塞式等待子进程
}
}
return 0;
}
我们还可以看到,当client进程没有读取时,命名管道的大小还是0,这说明数据并没有刷到磁盘中去。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
管道是需要进入内核的系统调用来进行数据的传输的(write、read……)。
管道通信的本质是基于文件的,而system V没有这种设计。system V是让不同进程的进程地址空间通过页表映射到同一块内存区域上,这快内存区域就叫做共享内存。这种形式提高了数据传输的效率。
系统开辟一块空间,通过页表映射到进程的地址空间上。其映射是在虚拟地址空间上开辟空间,让该空间在页表上建立新的映射关系,映射到共享内存(修改页表的映射关系)。
映射建立完成后,可以让不同的进程看到同一块资源。
shmget函数
#include
#iclude
功能:用来创建共享内存。
原型:int shmget(key_t key,size_t size,int shmflg);
参数:key:共享内存段的名字。
size:共享内存的大小。
shmflg:权限。由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
返回值:成功返回一个非负整数,该共享内存段的标识码。失败返回-1。
既然有共享内存这个设计,那么肯定也会有多个进程都来创建共享内存。创建好的共享内存不可能就放在那里不管了,OS会管理这些共享内存,就如同进程的创建会有PCB一样。OS会维护共享内存的数据结构,当然,这都是OS的事情。
<1> 怎么理解key? 怎么理解shmget函数的返回值?
答:使用共享内存先要找到共享内存,key就是共享内存的名字。key是内核中的。共享内存创建或者获取好了以后,会返回一个非负整数,这个非负整数是用来标识key的,是用户层面的,用来交给我们使用的。
<2> 如何理解size?
答:创建共享内存的大小。一般创建4096bity(4KB)个大小,也就是一页大小。磁盘的Date blocks中一块区域是4KB(这要看文件系统的设定),刚好对应。当然,我们也可以把共享内存的大小写成4097bity……但是,操作系统会把共享内存的大小设定成4096*2,也就是两倍的4096bity。但是我们看到的大小依然是4097bity。
<3> 权限
在创建共享内存中,我们只关心IPC_CREAT和IPC_EXCL。
IPC_CREAT:如果共享内存存在,返回共享内存,如果不存在,再创建。(在调用成功的情况下,一定会获得一个共享内存,但是无法七确认是否是新的)
IPC_EXCL:单独使用无意义,经常和IPC_CREAT组合使用。组合使用如果共享内存不存在,则创建。如果存在则出错返回。调用成功一定会获得全新的共享内存。
(通过key值来了解共享内存存不存在)
ftokt函数
#include
#include
功能:创建一个key值
原型:key_t ftok(const char *pathname, int proj_id);
参数:pathname:工程名称(路径名)
proj_id:工程id(数据)
我们可以任意指定,如果不成功,我们可以修改一下。
返回值:成功时,返回生成的密钥值。失败时,返回-1,其中errno表示stat(2)系统调用的错误。
我们通过上面两个函数就可以创建一个共享内存。
#include
#include
#include
#include
#define PATH_NAME "/home/cxy/text_cxy/c_11_22"
#define PROJ_ID 0x77
#define SIZE 4096
int main()
{
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0){
perror("fyok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid<0){
perror("shmget");
return 2;
}
printf("key:%x\n",key);
printf("shmid:%d\n",shmid);
return 0;
}
运行结果:
key:77011c7d
shmid:2
查看共享内存:ipcs -m
命令
大家有没有发现一个问题,我的进程退出了,查看共享内存时,共享内存为什么还存在?。
答:这是因为,进程结束,共享内存不会被释放。生命周期随内核的。进程不主动删除或用命令删除共享内存一直存在,直到关机重启。
shmctl函数
#include
#include
功能:用于控制共享内存
原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:shmid:key的标识符
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1。
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置未共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
我们删除共享内存只需要IPC_RMID。第三个参数设置NULL就行,现在不讲,后面会提这个结构体。
int main()
{
……
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
shmat函数
#include
#include
功能:将共享内存连接到进程地址空间
原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
参数:shmid:key的标识符
shmaddr:要连接到那一段进程地址空间的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向对应共享内存的映射到进程地址空间中的虚拟地址的起始地址;失败返回-1。
一般在设置shmaddr的时候设置为NULL,让操作系统去找一个地方挂接。因为在一般情况下,我们是无法指定进程地址空间上的地址的。第三个参数我们设置为默认值0,为可读可写的。
int main()
{
……
char* mem=shmat(shmid,NULL,0);
return 0;
}
说明:
shmaddr为NULL,内核会自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。
shmdt函数
关联完成后,在删除之前,我们要先去掉关联。
#include
#include
功能:将共享内存段与当前进程脱离
原型:int shmdt(const void *shmaddr)
参数:shnaddr:由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段。
int main()
{
……
char* mem=shmat(shmid,NULL,0);
shmdt(mem);
return 0;
}
shm.h
//shm.h
#include
#include
#include
#include
#define PATH_NAME "/home/cxy/text_cxy/c_11_22"
#define PROJ_ID 0x77
#define SIZE 4096
server.h
//server.h
#include"shm.h"
#include
int main()
{
key_t key=ftok(PATH_NAME,PROJ_ID);//得到一个key值
if(key<0){
perror("fyok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//创建一个新共享内存,名字为key,设置权限为0666
if(shmid<0){
perror("shmget");
return 2;
}
char *mem=shmat(shmid,NULL,0);//关联共享内存和进程地址空间,可读可写
while(1){
printf("server#%s\n",mem);
sleep(1);
}
shmdt(mem);//去关联
if(shmctl(shmid,IPC_RMID,NULL)<0){//释放共享内存
perror("shmctl");
return -1;
}
return 0;
}
client.h
#include
#include"shm.h"
int main()
{
key_t key=ftok(PATH_NAME,PROJ_ID);//得到key值
if(key<0){
perror("ftok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT);//找到key的共享内存
if(shmid<0){
perror("shmget");
return 2;
}
char* mems=shmat(shmid,NULL,0);//关联共享内存和进程地址空间
int i=0;
while(1){
mems[i]='a'+i;
i++;
mems[i]=0;
sleep(1);
}
shmdt(mems);//去关联
return 0;
}
运行结果:
注意:ctrl+C结束进程server,不会释放共享内存。
使用ipcs -m
可以查看共享内存。
我们已经知道了key 是共享内存的名称、shmid是key的标识符。
下面的是:
owner:用户
perms:权限(在创建共享内存时,可以设置,上面代码中可以看到)
bytes:共享内存大小(该大小是我们设置的大小,当我们设置4097时,bytes为4097,但实际大小为2*4097)
nattch:共享内存的关联数量(使用shmat函数关联的数量)
1、管道是通过文件来进行通信的。共享内存则是让不同的进程通过页表的映射看到同一块内存区域,进行通信时不用到内核中进行数据的传输,在效率上高于管道。
2、管道自带同步于互斥,共享内存不提供同步和互斥,共享内存需要自己去维护同步和互斥,这里可以通过信号量来完成。
3、管道的生命周期随进程,共享内存的生命周期随内核。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
其中struct ipc_perm shm_perm; /* operation perms */
数据结构中,就有key。key是在这个数据结构中的,权限也在。
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
通过类型来判断这个这块数据是谁发给谁的。
消息队列也有一块结构体,该结构体的第一个字段和共享内存的结构体字段相同。
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
柔性数组中存放的是指向ipc_perm结构体的地址。解引用就是消息队列的地址。
共享内存是不提供同步和互斥的,信号量就可以完成互斥。
首先我们要知道临界资源,访问临界资源的代码我们叫做临界区,我们需要保护的就是这块临界区,用信号量保护。
对于申请一块资源,进程就必须要占有这块资源吗?当然不是,就好比去电影院看电影,不是要到座位上去了,这个座位就是你的,而是买了票,这个座位就是你的。我们进程只要申请信号量成功了,就一定有你的资源。
信号量有二元信号量和多元信号量,这里我们讲的是二元信号量。
对于进程A和进程B,要访问同一块资源时,为了防止它们同时访问造成错误,我们使用信号量。
信号量本质是一个计数器,下面通过伪代码来表示出来。
PV操作保证了互斥,整体来看,一个进程访问共享内存,那么没有访问,要么访问完毕了,这种叫做原子性。
信号量保护了临界资源,但是信号量本身也是临界资源,谁来保护信号量呢?
其实不用谁来保护,PV操作,必须保证原子性,就是说信号量本身具有原子性。
进程A和进程B属于竞争申请。