(七)进程通信(本机IPC)

目录

1. 无名管道     

1.1 无名管道的通信原理 

1.2 无名管道的API

1.2.1 函数原型

1.2.2 无名管道特点

1.2.3 父子进程通信

1.3 无名管道有两个缺点

1.4 什么时候合适使用无名管道呢

2. 有名管道

2.1 为什么叫“有名管道”

2.2 有名管道特点

2.3 有名管道的使用步骤

2.4 有名管道API

System V IPC

(1)什么是System V IPC

(2)System V IPC的特点

(3)System V IPC标识符

3 System V IPC 之 消息队列    

3.1 消息队列的原理

(1)消息队列的本质

(2)消息是如何存放在消息队列中的呢?

(3)收发数据的过程

(4)使用消息队列实现网状交叉通信

3.2 消息队列的使用步骤

3.3    消息队列的函数

(1)msgget函数

(2)msgsnd

(3)msgrcv函数        

(4)进程结束时,自动删除消息队列        

3.4 什么时候合适使用消息队列

3.5 消息队列的缺点

4. System V IPC 之共享内存

4.1 共享内存介绍

4.2 共享内存原理

4.3 共享内存的使用步骤

4.4 共享内存的函数

4.4.1 shmget函数

4.4.2 shmat

4.4.3 shmdt函数 

4.4.4 shmctl函数

5. system V IPC  之  信号量(或信号灯)semaphore

5.1 信号量的作用

5.2 资源保护操作的种类

5.3 使用信号量实现互斥    

5.3.1 需要互斥实现“资源保护”的例子

5.3.2 进程信号量实现互斥的原理     

5.3.3 信号量相关的API

5.3 使用信号量实现同步

5.3.1 什么是同步

5.3.2 同步举例1

5.3.3 同步例子2


Linux提供的“进程通信”方式有哪些

 

1. 无名管道     

1.1 无名管道的通信原理 

(1)到底什么是管道

      内核的代码也是运行在物理内存上的,内核创建一个“管道”,其实就是在内核自己所在的物理内存空间中开辟出一段缓存空间

      

 

(2)如何操作无名管道

       以文件的方式来读写管道,以文件方式来操作时

        1)有读写用的文件描述符
        2)读写时会用write、read等文件Io函数
 

(3)为什么叫无名管道

      既然可以通过“文件描述符”来操作管道,那么它就是一个文件(管道文件),但是无名管道文件比较特殊,它没有文件名,正是因为没有文件名,所有被称为无名管道。

1.2 无名管道的API

1.2.1 函数原型

#include <unistd.h>

int pipe(int pipefd[2]);

  

 

1.2.2 无名管道特点

(1)无名管道只能用于亲缘进程之间通信,为什么?

  由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,

  所以只有一种办法,那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。

  然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道, 从而实现通信。

(2)读管道时,如果没有数据的话,读操作会休眠(阻塞)

  

1.2.3 父子进程通信

(1)父子进程单向通信

1)实现步骤 

  • (a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
  • (b)fork创建出子进程,子进程继承无名管道读、写文件描述符
  • (c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信

2)SIGPIPE信号

    写管道时,如果管道的读端被close了话,向管道“写”数据的进程会被内核发送一个SIGPIPE信号,

(2)父子进程双向通信

1)单个无名管道无法实现双向通信,为什么?

     因为使用单个无名管道来实现双向通信时,自己发送给对方的数据,就被自己给抢读到。

2)如何实现无名管来实现双向通信

    使用两个无名管道,每个管道负责一个方向的通信。

    

    

1.3 无名管道有两个缺点

(1)无法用于非亲缘进程之间
(2)无法实现多进程之间的网状通信

1.4 什么时候合适使用无名管道呢

                         

2. 有名管道

2.1 为什么叫“有名管道”

当我们调用相应的API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的 “有名管道文件”。

不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。

2.2 有名管道特点

2.2.1 能够用于非亲缘进程之间的通信(有文件名)

2.2.2 读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)

2.2.3 当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号

2.3 有名管道的使用步骤

(1)进程调用mkfifo创建有名管道

(2)open打开有名管道

(3)read/write读写管道进行通信


对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可。

为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,

2.4 有名管道API

#include <sys/types.h>
#include <sys/stat.h>
			
int mkfifo(const char *pathname, mode_t mode);

  

通信

同样的,使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题。

所以双向通信时需要两个管道。

什么时候使用有名管

(1)实现网状通信 (实现起来很困难)

(2)什么时候合适使用有名管道

  •       当两个进程需要通信时,不管是亲缘的还是非亲缘的,我们都可以使用有名管道来通信。
  •       至于亲缘进程,你也可以选择前面讲的无名管道来通信。
     

System V IPC

(1)什么是System V IPC

无名管道和有名管道,都是UNIX系统早期提供的比较原始的一种进程间通信(IPC)方式,早到Unix系统设计之初就有了

后来Unix系统升级到第5版本时,又提供了三种新的IPC通信方式: 消息队列 信号量 共享内存

System V就是系统第5版本的意思,后来的Linux也继承了unix的这三个通信方式

(2)System V IPC的特点

管道的本质就是一段缓存,不过Linux OS内核是以文件的形式来管理的,

System V IPC与管道有所不同,它完全使用了不同的实现机制,与文件没任何的关系

使用System V IPC时,不存在亲缘进程一说

(3)System V IPC标识符

System V IPC不再以文件的形式存在,因此没有文件描述符这个东西,但是它有类似的“标识符”。

3 System V IPC 之 消息队列    

3.1 消息队列的原理

(1)消息队列的本质

消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表 称为了消息队列

通信的进程通过共享操作同一个消息队列,就能实现进程间通信。

(2)消息是如何存放在消息队列中的呢?

消息队列这个链表有很多的节点,链表上的每一个节点就是一个消息。

从图中可以看出,每个消息由两部分组成,分别是消息编号(消息类型)和消息正文。

1)消息编号:识别消息用
2)消息正文:真正的信息内容

(3)收发数据的过程

1)发送消息

(a)进程先封装一个消息包

     这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文写到结构体的成员中。

struct msgbuf
{
			long mtype;         /* 放消息编号,必须> 0 */
			char mtext[msgsz];  /* 消息内容(消息正文) */
};	

(b)调用相应的API发送消息

       调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列,消息包(存放消息的结构体变量)会被作为一个链表节点插入链表。

 

2)接收消息

  调用API接收消息时,必须传递两个重要的信息,
(a)消息队列标识符    
(b)你要接收消息的编号

  有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号的消息

“消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,

 接收消息的人自己到公告牌上 去取对应编号的消息,如此,发送者和接受者之间就实现了通信。

(4)使用消息队列实现网状交叉通信

管道很难实现网状交叉通信,但是使用消息队列确非常容易实现。

3.2 消息队列的使用步骤

(1)使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID),后续收发消息就是使用这个标识符来实现的。

 

(2)收发消息

                · 发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
                · 接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息

 

(3)使用msgctl函数,利用消息队列标识符删除消息队列

3.3    消息队列的函数

(1)msgget函数

   1)函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

(a)功能:利用key值创建、或者获取一个消息队列

(b)返回值

(c)参数

(d)多个进程是如何共享到同一个消息队列的    

2)通信

(a)如何验证消息队列是否被创建成功?

        

(b)system v ipc的缺点

     进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。

     如何删除?

       

(2)msgsnd

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

(3)msgrcv函数        

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

  

(4)进程结束时,自动删除消息队列        

         我们需要调用msgctl函数来实现。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

(a)功能

(b)参数

struct msqid_ds 
{
		struct ipc_perm  msg_perm; /* 消息队列的读写权限和所有者 */
		time_t  msg_stime;    /* 最后一次向队列发送消息的时间*/
		time_t  msg_rtime;    /* 最后一次从消息队列接收消息的时间 */
		time_t  msg_ctime;    /* 消息队列属性最后一次被修改的时间 */
		unsigned  long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
		msgqnum_t  msg_qnum;     /* 队列中当前消息的条数*/
		msglen_t msg_qbytes;  /* 队列中允许的最大的总的字节数 */
		pid_t  msg_lspid;     /* 最后一次向队列发送消息的进程PID */
		pid_t  msg_lrpid;     /* 最后一次从队列接受消息的进程PID */
};
		
struct ipc_perm 
{
	key_t          __key;       /* Key supplied to msgget(2):消息队列的key值 */
	uid_t          uid;         /* UID of owner :当前这一刻正在使用消息队列的用户 */
	gid_t          gid;         /* GID of owner :正在使用的用户所在用户组 */
	uid_t          cuid;        /* UID of creator :创建消息队列的用户 */
	gid_t          cgid;        /* GID of creator :创建消息队列的用户所在用户组*/
	unsigned short mode;        /* Permissions:读写权限(比如0664) */
	unsigned short __seq;       /* Sequence number :序列号,保障消息队列ID不被立即
																	重复使用 */
};

 

3.4 什么时候合适使用消息队列

当你的程序必须涉及到多进程网状交叉通信时,消息队列是上上之选。

3.5 消息队列的缺点

与管道一样,不能实现大规模数据的通信,大规模数据的通信,必须使用后面讲的“共享内存”来实现。

4. System V IPC 之共享内存

4.1 共享内存介绍

共享内存的API与消息队列的API非常相似,应该System V IPC的API都是差不多的

共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。

直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。

4.2 共享内存原理

以两个进程使用共享内存来通信为例,实现的方法就是:

(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
          

4.3 共享内存的使用步骤

4.4 共享内存的函数

4.4.1 shmget函数

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

  

4.4.2 shmat

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

  

4.4.3 shmdt函数 

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);	

  

4.4.4 shmctl函数

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    

struct shmid_ds 
{
	struct ipc_perm shm_perm;    /* Ownership and permissions:权限 */
	size_t shm_segsz;   /* Size of segment (bytes):共享内存大小 */
	time_t shm_atime;   /* Last attach time:最后一次映射的时间 */
	time_t shm_dtime;   /* Last detach time:最后一次取消映射的时间 */
	time_t shm_ctime;   /* Last change time:最后一次修改属性信息的时间 */
	pid_t shm_cpid;    /* PID of creator:创建进程的PID */
	pid_t shm_lpid;    /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
	shmatt_t shm_nattch;  /* No. of current attaches:映射数量,
												 * 标记有多少个进程空间映射到了共享内存上
												 * 每增加一个映射就+1,每取消一个映射就-1 */ 
	...
};

struct ipc_perm,这个结构体我们在讲消息队列时已经讲过,这里不再重复讲。
struct ipc_perm 
{
	 key_t          __key;    /* Key supplied to shmget(2) */
	 uid_t          uid;      /* UID of owner */
	 gid_t          gid;      /* GID of owner */
	 uid_t          cuid;     /* UID of creator */
	 gid_t          cgid;     /* GID of creator */
	 unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */
	 unsigned short __seq;    /* Sequence number */
};

 

5. system V IPC  之  信号量(或信号灯)semaphore

5.1 信号量的作用

简洁一点,信号量用于“资源的保护“。

5.2 资源保护操作的种类

(1)互斥

            对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题

            它们只关心一件事,那就是我在操作时别人不能操作。

(2)同步

           所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作。    

(3)实现同步、互斥,其实就是加锁

           信号量就是一个加锁机制,通过加锁来实现同步和互斥。

(4)疑问:信号量既然是一种加锁机制,为什么进程信号量会被归到了进程间通信里面呢?

           资源保护时,某个进程的操作没有完全完成之前,别人是不能操作的,

           那么进程间必须相互知道对方的操作状态,必须会涉及到通信过程。

           所以信号量实现资源保护的本质就是,通过通信让各个进程了解到操作状态,然后查看自己能不能操作。

5.3 使用信号量实现互斥    

进程信号量既能实现进程的互斥,也能实现进程的同步,不过有些“资源保护机制”就只能实现互斥

5.3.1 需要互斥实现“资源保护”的例子

                  

因为在切换进程时,往往只写了一个“hello”或者“hhhhh”后,就被切换到另一个进程,该进程会继续写数据,如此就对上一个进程所写数据产生了隔断。

5.3.2 进程信号量实现互斥的原理     

(1)什么是进程信号量

简单理解的话,信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。

(2)多值信号量和二值信号量

1)二值信号量

      同步和互斥时使用的都是二值信号量。

      二值信号量的值就两个,0和1,0表示不可以操作,1表示可以操作。
      通过对变量进行0、1标记,就可以防止出现相互干扰情况。

                   

2)多值信号量

      信号量的最大值>1,比如为3的话,信号量允许的值为0、1、2、3。

(3)信号量集合

号量其实是一个OS创建的,供相关进程共享的int变量,只不过我们在调用相关API创建信号量时,我们创建的都是一个信号量集合,所谓集合就是可能会包含好多个信号量。

   用于互斥时,集合中只包含一个信号量。

   用于同步时,集合中会包含多个信号量,至于多少个,需要看情况。

(4)信号量的使用步骤

1)进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。

2)调用semctl函数给集合中的每个信号量设置初始值

3)调用semop函数,对集合中的信号量进行pv操作

4)调用semctl删除信号量集合

 

什么是pv操作?

 pv操作其实说白了就是加锁、解锁操作

 (a)P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞

 (b)V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题

 总之通过pv操作(加锁、解锁),就能够实现互斥,以防止出现干扰。

5.3.3 信号量相关的API

(1)semget函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
	
//sem就是semaphore的缩写。

  

(2)semctl函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

(a)功能

根据cmd的要求对集合中的各个信号量进行控制,...表示它是一个变参函数,如果第四个参数用不到的话,可以省略不写。

(b)返回值

调用成功返回非-1值,失败则返回-1,errno被设置。

(c)参数说明

       

从以上可以看出,第四个参数对应内容是变着的,为了应对这种变化就用到了一个联合体。

union semun {
	 int              val;    //存放用于初始化信号量的值
	 struct semid_ds *buf;    //存放struct semid_ds结构体变量的地址
	 unsigned short  *array;  /* 不做要求 */
	 struct seminfo  *__buf;  /* 不做要求 */
};

这个联合体类型并没有被定义在信号量相关的系统头文件中,我们使用这个联合体时,我们需要自己定义这个类型,至于联合体类型名可以自己定,不过一般都是直接沿用semun这个名字

疑问:这个联合怎么用?

      

(3)semop函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);

- 结构体成员

struct sembuf
{
	unsigned short sem_num;  
	short          sem_op;
	short          sem_flg;  
}

这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。

  

5.3 使用信号量实现同步

5.3.1 什么是同步

让多个进程按照固定的步调做事,同步本身就是互斥的。

5.3.2 同步举例1

通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。

如何实现同步 (多米诺骨牌

  

可以看出,有多少个进程需要同步,我们在集合中就需要创建对应数量的信号量。

5.3.3 同步例子2

使用信号量来解决共享内存的同步问题

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章