Liunx学习之-----IPC---(进程间通信)

本文详细介绍了Linux进程间通信(IPC)的几种方式,包括匿名管道、命名管道、共享内存、消息队列和信号量。重点阐述了每种通信方式的概念、创建方法、操作原理及特性。例如,管道具有半双工通信性质,共享内存提供了最快捷的IPC途径,而信号量则用于实现进程间的同步与互斥。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


1.进程间通信在linux下是非常重要的,我们知道在linux下不同的进程的操作是互不干扰的( 独立性,这是因为每个进程都有自己的独特的虚拟地址,而这个虚拟地址就是进程与物理内存沟通的桥梁,对于不同的进程,相同的虚拟地址位置会映射到不同的物理内存上,所以进程具有独立性),那么我们难免会遇到一个进程需要另一个进程的东西,那么对于进程间通信,在这些方面就非常重要。

2.进程间通信的目的:

  • 数据传输:一个进程将自己的数据传输给另一个进程。
  • 资源共享:多个进程共享同一个资源。
  • 通知事件:一个进程要给另一个进程去发送消息。
  • 进程控制:一个进程想要去控制另一个进程,此时的控制想要拦截另一个进程的所有陷入和异常,并能够及时知道其状态的改变。

管道

1.概念:管道为进程间通信的一种方式,如同在生活中一样,管道对于我们的作用非常的大,它可以帮助我们合理,有条的去输送和排放东西。而在进程间通信中,管道分为以下两种管道:匿名管道和命名管道,并且他们具有半双工通信的功能。(两者有不同的适用情况)

2.半双工通信:一个管道有两个端口,每个端口都可以进行写入,或者读取资源,但是同一实际,相同的端口只能写入或者删除,不可同时进行。

3.管道的本质:本质为内核的一个缓冲区(内核的一块物理内存),通过多个进程访问同一个缓冲区来实现通信(由于内核对所有的进程来说都是一样的,所以在内核中开辟一块内存用于交流)。

匿名管道

1.概念:在内核中开辟这块内存,但是没有标识符,无法被其他进程找到。(所以匿名管道是针对于有亲缘关系的进程使用,例如:父子进程,由于子进程在通过父进程创建的时候,复制了父进程的文件描述信息,所以子进程也就有这个文件描述符去操作这个管道)
所以说,对于匿名管道,只有通过子进程去复制父进程的方式,才能获取同一个管道的操作句柄

2.匿名管道的创建:

①:创建函数接口:int pipe(int pipefd[2])
其中:

  • pipefd[2]:为管道的两个文件描述符,分别代表的是读取和写入。(其中,pipefd[0]表示的是从管道中读取数据,pipefd[1]表示的给管道中写入数据,成功返回0,失败返回-1)

注意:匿名管道的建立一定要在创建子进程之前,这样才能让子进程获得管道操作句柄。
②:匿名管道的操作原理:
如下图①:
在这里插入图片描述
为匿名管道的内核与父子进程的关系图。
而对于父子进程的操作管道缓冲去的时候如下图:
在这里插入图片描述
其中图中的3,4分别为读取和写入端。
如下:我们对匿名管道进行操作:

  1 #include<stdio.h>                                                                              
  2 #include<sys/wait.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 #include<string.h>
  6 int main()
  7 {
  8   int pipefd[2];
  9   char buf[1024];
 10 
 11   if(pipe(pipefd) == -1)//创建匿名管道
 12   {
 13     perror("pipe error");
 14     return -1;
 15   }
 16   pid_t pid = fork();
 17   if(pid < 0)
 18   {
 19     perror("fork error");
 20     return -1;
 21   }
 22   else if(pid > 0)//父进程向管道写入数据
 23   {
 24     close(pipefd[0]);
 25     write(pipefd[1],"i am father",12);
 26   }
 27   else
 28   {
 29     close(pipefd[1]);
 30     int res = read(pipefd[0],buf,12);
 31     printf("%s\n",buf);
 32     printf("%d\n",res);
 33   }
 34   wait(NULL);
 35   close(pipefd[0]);
 36   close(pipefd[1]);
 37   return 0;
 38 }            

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

  • 关闭文件描述符并不是释放缓冲区,也不是删除文件,只是断开了和管道的一端的连接。
  • 缓冲区是当所有的进程都和其断开后,缓冲区才会释放。
  • 缓冲区大小有限。

3.管道的特性:

  1. 如果管道中没有数据,则read堵塞,如果管道中数据满了,则write堵塞。
  2. 管道的读写操作是会堵塞的,如果对于一个管道,其写端全部关闭(不再向管道中写数据了),那么读端会将会将管道中的数据全部拿出,然后不再堵塞,返回0(这里的0表示的是不再写入的,因为是读,所以对读取文件没有操作,所以说,人家文件中还是有数据的)。
  3. 管道中的读端关闭了(没有人去写入数据了),那么write会出发异常,进程退出。
  4. 在匿名管道中,数据是先入先出的。
  5. 进程退出,管道释放,所有管道生命周期是跟随其进程的。
  6. 内核会对管道的操作进行同步和互斥。

命名管道

对于进程间的通信不可能只有父子间的通信,肯定还有没有亲缘关系的进程间通信,而对于这些进程,是通过命名管道进行通信的。

1.概念:内核中开辟这一块缓冲区,并具有标识符,可以被其他进程找到。(适用于同一主机中没有亲缘关系的进程间通信)
对于一个进程创建了命名管道,这个命名管道会在文件系统中创建出一个管道文件(实际上是管道的名称),多个进程通过打开同一个管道文件,访问内核中的同一个缓冲区实现通信。

2.命名管道的创建方法:
①:命令:mkfifo filename //创建命名管道文件
②:函数接口:int mkfifo(char* filename,mode_t mode)
其中:

  • filename:为创建的命名管道文件的文件名
  • mode:为文件的管理操作。

③:对于命名管道文件,其与内核的关系如图:
在这里插入图片描述
对于其的操作,如下代码:
①:为写端:

    1 #include<stdio.h>                                                                            
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 #include<string.h>             
    5 #include<errno.h>
    6 #include<fcntl.h>
    7 #include<sys/stat.h>     
    8 int main()    
    9 {  
   10   int res = mkfifo("tp",0664); 
   11   if(res < 0 && errno != EEXIST)      
   12   {          
   13     perror("mkfifo error");
   14     return -1;
   15   }                       
   16   int fd = open("tp",O_WRONLY);
   17   if(fd < 0)
   18   {         
   19     perror("open error");
   20     return -1;
   21   }
   22   char* buf = "i am process A";
   23   int ret = write(fd,buf,strlen(buf));
   24   if(ret < 0)
   25   {
   26     close(fd);
   27     perror("write error");
   28     return -1;
   29   }
   30   close(fd);
   31   return 0;
   32 }         

②:为读端:

  1 #include<stdio.h>                                                                              
  2 #include<stdlib.h>
  3 #include<unistd.h>         
  4 #include<string.h>
  5 #include<errno.h>
  6 #include<errno.h>              
  7 #include<fcntl.h>
  8 #include<sys/stat.h>
  9 int main()             
 10 {                             
 11   int res = mkfifo("tp",0664);
 12   if(res < 0 && errno != EEXIST)
 13   {                      
 14     perror("mkfifo error");
 15     return -1;
 16   }
 17   int fd;            
 18   fd = open("tp",O_RDONLY);
 19   if(fd < 0)
 20   {
 21     perror("open outfd error");
 22     return -1;
 23   }
 24   char buf[1024] = {0};
 25   int ret = read(fd,buf,1024);
 26   if(ret < 0)
 27   {
 28     perror("read error");
 29     close(fd);
 30     return -1;
 31   }
 32   printf("%s\n",buf);
 33   close(fd);
 34   return 0;
 35 }        

操作方式为,先向一个缓冲区去写入数据,再向其中读出数据,如下是操作结果:
在这里插入图片描述
首先运行读取行为,此时处于阻塞状态,然后我们打开另一个进程,去运行写端函数
在这里插入图片描述
然后返回来看读端的结果:
在这里插入图片描述
然后就运行出来了。

3.命名管道的特性:

  1. 创建命名管道文件,并不会立即创建缓冲区,而是在有进程访问的时候才会创建(写时拷贝思想,提高资源,节省效率)。
  2. 如果命名管道文件只以读打开,那么就会堵塞,会一直堵塞到这个文件被其他程序以写打开。
  3. 如果命名管道文件只以写打开,那么就会堵塞,会一直堵塞到这个文件被其他程序一读打开。

特性2和3的原理:对于一个命名管道文件,在没有确定要写入数据和有进程去读取数据的时候,就没有必要去开辟,会浪费资源。

其他特性和匿名管道相同。
4.对于管道自带的同步和互斥
①:同步:按照一定的顺序去进行(写入数据的时候才能有数据被读取,没有数据,则read堵塞,数据满了,则write堵塞,读取了继续写)。
②:互斥:操作是安全可靠的(对于两个进程对一个管道文件写入的时候必须有数据,防止交叉写入,破坏数据原本的样子);并且读写大小不能超过PIPE_BUF(也就是4096字节),大小保证原子操作。
③:原子操作:一次性完成,中间不能被打断。

共享内存

1.特性:是最快的进程间通信(IPC)的一种方法。

2.原理:因为对于共享内存,他是在物理内存上开辟一个内存块,然后对于有需要的的进程,会连接这个内存块,将其映射到自己的虚拟地址上,这样对于不同的进程对于这个内存的操作就不会涉及内核了,所以大大的减少了内核和用户直接沟通的那段时间,提高了效率,所以是最快的IPC。

如下图:
在这里插入图片描述
3.共享内存的相关函数:

  • shmget函数(创建共享内存):int shmget(key_t key,size_t size,int shmflg);
    其中:
    ①:key为共享内存段的名字,为了让多个进程找到同一个。(其实key_t也是一个int的数据类型,而这个内存段的名字其实就是没每个内存所对应的id)其中,如果key为IPC_PRIVATE(也就是0)的话,则会建立新的内存对象,而自己设置为0~32位的数时要视参数shmflg来确定操作。通常要求此值来源于ftok返回的IPC键值。而对于ftok的函数定义如下:key_t ftok(const char* filename,int proj_id)其中proj_id为项目工程的一个号,是由自己定义出,而对这个函数的操作,是将文件的编号和proj_id号合并的,而文件的编号可由如下求出,如下图:
    在这里插入图片描述
    ②:size:需要创建共享内存的大小。(仅创建时候有效)
    ③:shmflag:一般情况下,我们会写:IPC_CREAT|IPC_EXCL|0664
    注意:
    IPC_CREAT:表示不存在则创建,存在则打开。
    IPC_EXCL:表示存在则报错,不存在则创建打开,与IPC_CREAT搭配使用。
    0664:表示的是对这个内存的访问权限。(文件创建的时候必须加上,不然会随机给文件操作权限,会出现一些不堪的情况)
    ④:成功返回一个非负整数----操作句柄,失败返回-1。

  • shamt函数(建立进程与共享内存间映射关系):void* shmat(int shm_id,void* shmaddr,int shmflag)
    其中:
    ①:shm_id:shmget函数的返回值(也就是共享内存的操作句柄)。
    ②:shmaddr:通常设置为NULL,让系统自动建立映射关系。
    ③:shmflag:设置为SHM_RDONLY为只读,0为默认的可读可写。
    返回值:成功返回映射的首地址,失败返回(void*)-1。

  • shmdt函数(将共享内存与当前进程断开):int shmdt(void* shmaddr)
    其中:
    ①:shmaddr为shamt函数的返回值。
    返回值:成功返回0,失败返回-1。

  • shmctl函数(控制共享内存):int shmctl(int shmid,int cmd,struct shmid_ds *buf)
    其中:
    ①:shmid:shmget函数的返回值。
    ②:cmd:IPC_RMID-标记函数(不会再接受新的映射)
    ③:buf:用于获取共享内存的信息,,不需要的话设置为NULL。
    返回值:成功返回0,针对IPC_RMID的失败返回-1。
    cmd一共有三个情况可取如下图:
    在这里插入图片描述

4.共享内存的操作流程为以下:

  1. 创建或者打开共享内存
  2. 将共享内存映射到相应的进程虚拟地址空间中
  3. 对其进行操作
  4. 解除映射关系
  5. 删除共享内存

对于删除共享内存时候要注意的是:共享内存的删除是一种计数器的形式,当所有进程对其进行删除的时候(意思为计数器的数量为0的时候),共享内存才会被删除,并且释放其中的资源。

以下为共享内存的例子:
如下为写入:

 1 #include<stdio.h>                                                
    2 #include<stdlib.h>
    3 #include<string.h>
    4 #include<unistd.h>                            
    5 #include<sys/types.h>
    6 #include<sys/ipc.h>
    7 #include<sys/shm.h>        
    8               
    9 #define KEY 0x11111111
   10 int main()                              
   11 {                           
   12   int shmid = shmget(KEY,4096,IPC_CREAT|0664);
   13   if(shmid < 0)           
   14   {           
   15     perror("shmget error");
   16     return -1;
   17   }                
   18   void*shm_start =  shmat(shmid,NULL,0);
   19   if(shm_start == (void*)-1)
   20   {
   21     perror("shmat error");
   22     return -1;
   23   }                        
   24   snprintf(shm_start,4096,"i am process A");
   25   shmdt(shm_start);
   26   shmctl(shmid,IPC_RMID,NULL);          
   27   return 0;                 
   28 }               

如下为读取:

  1 #include<stdio.h>                                                                              
  2 #include<stdlib.h>
  3 #include<unistd.h>                            
  4 #include<sys/types.h>
  5 #include<sys/ipc.h>
  6 #include<sys/shm.h>        
  7               
  8 #define KEY 0x11111111
  9 int main()                              
 10 {                           
 11   int shmid = shmget(KEY,4096,IPC_CREAT|0664);
 12   if(shmid < 0)           
 13   {           
 14     perror("shmget error");
 15     return -1;
 16   }                               
 17   void*shm_start =  shmat(shmid,NULL,0);
 18   if(shm_start == (void*)-1)  
 19   {        
 20     perror("shmat error");    
 21     return -1;
 22   }                        
 23   sleep(10);  
 24   printf("%s\n",(char*)shm_start);
 25   shmdt(shm_start);                     
 26   shmctl(shmid,IPC_RMID,NULL);
 27   return 0;
 28 }  

运行结果如下:
先在写入端进程去写入:
在这里插入图片描述
然后在读取端函数进行读取,如下:
在这里插入图片描述
对于查看和删除进程间通信的还有两个指令:
①:ipcs指令查看。
②:ipcrm指令删除。

5.共享内存的特性:

  • 共享内存是一种覆盖式操作。(写入数据时候会直接覆盖前面的内容)
  • 共享内存的生命周期跟随的是内核,在非人为的操作下,共享内存即使连接数为0也不会去释放。
  • 共享内存中没有互斥同步关系。

消息队列

1.本质:是内核中的一个优先级队列。
2.功能:具有标识符,可以被其他进程找到,多个进程通过访问同一个队列,通过添加或者获取节点实现通信。
3.传输的节点:消息队列传输的都是数据节点,并且节点中包含两个信息:类型和数据。(类型的作用是用于身份区分)
其队列的简单图如下:
在这里插入图片描述
其中用head和tail来识别要取出的数据段,而1和n代表的节点。
特点:
①:双工通信。
②:自带互斥与同步。
③:生命周期跟随内核。

信号量

1.本质:是内核中的一个计数器。

2.作用:实现进程间的同步与互斥(包含进程间对临界资源的访问操作)。
其中:临界资源就是大家都能访问到的资源,也就是我们需要传输的资源。

3.对于保护操作的方式也就是我们上面所说的同步和互斥:
①:同步:通过一些条件让资源访问有序。
②:互斥:通过让进程同一时间对资源进行唯一访问来保证资源安全。

4.信号量对同步和互斥的操作原理:
会通过计数器进行计数,若计数器大于0则表示可以访问资源,如果小于或者等于0,则不能去访问资源,进程堵塞。
其操作方式如下:
会有两个操作方式,一个管理输入,一个人管理输出。
①:P操作:管理输入,在进程被访问之前进行,首先判断计数是否大于0
如果大于0,则计数器-1,如果小于0计数器也是-1。(用来排队使用)
②:V操作:管理输出,当一个资源被操作完后,没有操作的时候,那么计数器就会+1,则会有一个空位,在外面排队的就可以进入一个。

5:对互斥和同步的擦着步骤:
①:互斥:

  • 初始化临界资源计数器为1。
  • 在访问临界资源之前进行P操作。
  • 在访问临界资源之后进程V操作。

②:同步:

  • 根据资源数量初始化计数器。
  • 访问资源之前进行P操作。
  • 产生一个新资源进行V操作。

对于互斥和同步就像是在停车场停车一样,同步是在停车场门口进行的操作,排队驶入,而互斥就像是停车场内部对一个停车位进行的操作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值