Linux - 第7节 - 进程间通信

目录

1.进程间通信介绍

2.管道

2.1.管道的概念

2.2.匿名管道

2.3.命名管道

3.System V IPC

3.1.system V共享内存

3.2.system V消息队列(选学了解)

3.3.system V信号量(选学了解)

3.4.System V IPC补充知识


1.进程间通信介绍

进程间通信目的:

\bullet 数据传输:一个进程需要将它的数据发送给另一个进程 。
\bullet 资源共享:多个进程之间共享同样的资源。
\bullet 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
\bullet 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信背景:

要完成某个功能,可能需要多进程进行协同处理。进程是具有独立性的,进程间想交互数据,成本会非常高。

进程间通信发展:

\bullet 管道
\bullet System V进程间通信
\bullet POSIX进程间通信

进程间通信分类:
管道System V IPCPOSIX IPC
\bullet 匿名管道pipe
\bullet 命名管道
\bullet System V 消息队列
\bullet System V 共享内存
\bullet System V 信号量
\bullet 消息队列
\bullet 共享内存
\bullet 信号量
\bullet 互斥量
\bullet 条件变量
\bullet 读写锁

2.管道

2.1.管道的概念

引入:

进程具有独立性,进程之间如果要进行通信,在通信之前要让不同的进程看到同一份资源(文件、内存块等) 。我们要学的进程间通信,不是告诉我们如何通信,而是如何让两个进程在内存中看到同一份资源,而资源的不同决定了不同种类的通信方式。

这里要学习的管道就是提供共享资源的一种手段。

管道的原理:

进程打开文件,进程task_struct指向的files_struct中数组struct file*fd_array[]会有一个下标,其对应的内容指向打开文件的struct_file(文件描述符fd指向文件的struct_file)。如果该进程创建了子进程,那么子进程会拷贝该进程的task_struct和files_struct部分,对PID等部分内容进行修改,文件的struct_file不会进行拷贝。因为子进程拷贝了父进程的files_struct,这就意味着父进程的struct file*fd_array[]数组与子进程的相同,即父进程打开的文件子进程也打开了。

如下图一所示,如果只创建一个struct_file而该struct_file不与真实文件映射,这种文件就称为管道或管道文件,父进程和子进程同时打开管道文件,父进程向管道文件写入,存储在管道文件的内部缓冲区中,子进程在内部缓冲区中读取数据,这样就完成了进程间通信。

因此管道的本质是一个内存级文件。

如下图二所示,父进程创建管道时,使用两个文件描述符,分别以读方式和写方式打开同一个管道,父进程创建子进程后,子进程继承了父进程的struct file*fd_array[]数组,因此子进程也是分别以读方式和写方式打开该管道。管道是单向读写的,因此父进程关闭以读(写)方式打开的管道文件,子进程关闭以写(读)方式打开的管道文件,父子进程各自关闭不需要的读写端,这样就建立了单项通信的信道。

注:

1.struct file中包含了文件的所有属性,例如操作方法、file自己的内部缓冲区等。

2.当我们使用不同的调用或命令,操作系统能够识别要使用的是普通文件、管道文件还是字符设备文件(鼠标键盘等),在struct file中有一个union联合体,其记录自己是普通文件、管道文件还是字符设备文件的struct file,如果是普通文件和字符设备文件,对应struct file指向底层设备,可以向设备刷新写入或读取数据,如果是管道文件 ,对应的struct file不进行指向。

问题1:为什么父进程创建管道时,要分别打开管道的读和写?

答:父进程创建管道时,分别以读和写的方式打开管道,是为了让子进程继承(继承其struct file*fd_array[]数组),这样子进程就不用再打开管道了。

问题2:为什么父子进程要各自关闭对应的读写端?

答:因为管道必须是单向通信的。

问题3:父子进程对应关闭读或写哪一个端口,是由谁决定的?

答:父子进程对应关闭读或写哪一个端口,不是由管道本身决定的,而是由需求决定的。

2.2.匿名管道

创建匿名管道:

Linux提供了创建匿名管道的pipe接口,pipe接口会创建一个管道,并分别以读和写的方式打开管道文件,将读和写打开管道文件的对应两个文件描述符写入pipefd数组中,作为输出型参数返回。其中pipefd数组0下标的文件描述符对应读文件,pipefd数组1下标的文件描述符对应写文件。

返回值是int类型,如果返回0代表成功,返回-1代表失败。

注:

1.pipe是一个系统调用,创建文件时会将内核中struct file的union联合体的i_pipe指针进行初始化(不初始化*_bdev和i_cdev指针),因此不会和文件或硬件产生关联。

2.使用pipe接口需要包含unistd.h头文件。

匿名管道通信的代码实现:

使用xhell创建一个pipe目录,使用vscode进入pipe目录,创建一个pipe.cc文件,写入下图一所示的代码,创建一个makefile文件,写入下图二所示的代码,然后使用make命令生成mypipe可执行程序,./mypipe运行可执行程序,运行结果如下图三所示。

pipe.cc文件中的代码创建了子进程,在父子进程之间用管道连接,父进程写入子进程读取。

注:

1.父子进程通过管道文件传输数据,管道文件struct file中有一个计数,记录连接管道文件的文件数,如果文件数为2则说明父子进程正在进行管道传输,如果文件数为1则说明写入的一方已将写入管道文件关闭,此时读取方感知到文件数为1,即写入方已经关闭,只要将管道中的数据读取完就可以结束了。

在子进程部分循环读取管道中的内容,如果子进程感知到父进程已将写入管道文件关闭,此时读取完管道中的内容,读取完后下一次read读取为空就会返回0,因此只要read返回0则说明父进程已关闭写入管道文件且管道内数据已读取完毕,此时子进程就可以退出了。

问题:上面的代码中,子进程没有带sleep函数,为什么子进程也会休眠呢?且休眠时间为什么和父进程相同呢?

答:当父进程没有写入数据时(即管道内部没有数据时),子进程read时就会阻塞等待,当父进程写入之后(即管道内部有数据时),子进程才会read返回数据,所以子进程读取打印数据是以父进程的节奏为主。如果父进程写入数据将管道写满了,那么父进程再write写入时就会阻塞等待,等待管道内数据被子进程读取走有空间后再进行write写入。

从这里也可以看出父进程和子进程通过管道进行读写的时候,是会互相等待的,有一定的顺序性,因此在pipe内部自带访问控制机制(对应后面在多线程部分会讲的同步和互斥机制)。当父子进程各自printf向显示器写入时,是没有顺序的,因此是缺乏访问控制的。

注:父子进程通过管道进行读写的时候,是会互相等待的,这里的等待阻塞等待,即将对应进程的状态由S改为S/D/T,并将该进程的task_struct放入等待队列中。

补充:通过管道进行写入时,当一次性要写入的数据量不大于PIPE_BUF时(PIPE_BUF是一个宏,为4096字节),linux将保证写入的原子性(原子性是访问控制的一种,后面会讲到),当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

匿名管道通信的应用1:(父进程向一个子进程派发任务)

使用xhell创建一个pipe目录,使用vscode进入pipe目录,创建一个pipe.cc文件,写入下图一所示的代码,创建一个makefile文件,写入下图二所示的代码,然后使用make命令生成mypipe可执行程序,./mypipe运行可执行程序,运行结果如下图三所示。

pipe.cc文件中的代码创建了一个子进程,在父子进程之间用管道连接,父进程向管道中写入操作码,子进程读取操作码完成对应的操作。

注:在子进程部分使用assert函数进行断言,确保读取到的字节数和我们设置读取的字节数相等,即确保成功读取。assert函数后面的(void)s代码有什么用呢?assert断言只在debug模式下编译有效,在release模式下断言就没有了,一旦断言没有了,s变量就是只被定义没有使用,那么在release模式下可能会有warning警告,(void)s代码的作用就是使用一下s变量,消除掉release模式下的警告。

匿名管道通信的应用2:(父进程向多个子进程派发任务)

使用xhell创建一个pipe目录,使用vscode进入pipe目录,创建一个pipe.cc文件,写入下图一所示的代码,创建一个makefile文件,写入下图二所示的代码,然后使用make命令生成mypipe可执行程序,./mypipe运行可执行程序,运行结果如下图三所示。

pipe.cc文件中的代码创建了多个子进程,在每一对父子进程之间都用一个管道连接,父进程向不同的管道中写入操作码,对应的子进程读取操作码完成相应的操作。

注:blanceSendTask函数是随机选择一个进程派发任务,没有压着一个进程给任务,像这样较为均匀的将任务给所有的子进程,我们叫做负载均匀。

命令行中使用的管道:

我们在命令行中通常使用符号“|”作为管道传输数据,例如cat test.c | wc -l、sleep10000 | sleep 1000等。以sleep10000 | sleep 1000等为例,如下图所示,执行sleep 10000进程和执行sleep 1000进程的父进程相同,也就是说这里两个子进程在通信。

这里父进程先创建一个管道,然后创建执行sleep 10000的子进程1,子进程1的files_struct中数组struct file*fd_array[]继承父进程,因此子进程1打开了管道的读写端,然后父进程创建执行sleep 1000的子进程2,子进程2的files_struct中数组struct file*fd_array[]继承父进程,因此子进程2也打开了管道的读写端,此时父进程关闭管道的读写端,进程1和进程2关闭管道对应的读或写端,这样进程1和进程2就能够通过管道传输数据了。

注:这里在命令行中使用的“|”其实就是匿名管道。

管道的特点总结:

\bullet 管道只能单向通信(内核实现决定的) ,是半双工的一种特殊情况。

\bullet 管道自带访问控制或同步机制(pipe满,writer等;pipe空,reader等)。

\bullet 管道是面向字节流的。管道在系统中是一个具有固定大小的缓冲区(一般是4KB),管道中先写的字符一定是先被读取的。管道中的内容没有格式边界,需要用户来定义区分内容的边界,也就是说管道中只是一个个字节,如何组织读取这些字节获得有用内容由用户代码决定,例如read(blockFd, &operatorCode, sizeof(uint32_t))代码中的sizeof(uint32_t),其每次读取四个字节。

\bullet 管道的生命周期。如果一个被打开的文件其引用计数变为0(即没有进程打开该文件了),那么该文件就会被关闭。进程退出了,被进程打开的文件就会自动关闭。管道也是文件,当借助管道通信的两个进程关闭了管道文件或借助管道通信的两个进程退出了,那管道也会被关闭,因此管道的生命周期与使用管道的进程有关。

匿名管道独有特点:

\bullet 匿名管道只能用于具有亲缘关系的进程之间进行通信,常用于父子进程间通信。

2.3.命名管道

命名管道引入:

匿名管道只能用于具有亲缘关系的进程之间进行通信,如果毫不相关的进程之间想进行通信,需要使用命名管道。

进程间通信的本质是不同的进程看到同一份资源,匿名管道通过fork创建子进程后,子进程继承父进程文件描述符表struct file*fd_array[]的特性做到的,命名管道则是通过两个进程打开同一个管道文件做到的。

注:

1.两个进程打开同一个文件,内核中文件的struct file其实只有一份,只不过每打开一次,struct file里面的引用计数加一。

2.命名管道传输两个进程间的通信数据时,并不会将数据刷新到磁盘的文件中,磁盘中的管道文件仅仅是一种符号,以供不同进程看到,两个进程打开命名管道,其通信依旧是在内存中进行的。

创建命名管道的命令:
Linux提供了创建命名管道的mkfifo命令,mkfifo命令用来创建命名管道文件,如下图所示。
注:创建的命名管道文件中权限部分第一个字母是p,以p开头的就是管道文件。
命名管道命令方式通信的简单实现:
创建一个命名管道myfifo,使用命令echo "aaaaa" > myfifo创建进程向管道文件中写入内容,使用命令cat < myfifo创建进程读取管道文件内容到显示器上,如下图一所示。
使用命令while :; do echo "aaaaa" ;sleep 1;done >> myfifo创建进程向管道文件中持续写入内容,使用命令cat < myfifo创建进程读取管道文件内容到显示器上,如下图二所示。

创建命名管道的接口函数:
Linux提供了创建命名管道的mkfifo接口,mkfifo接口用来创建命名管道文件,如下图所示。参数pathname是制作命名管道文件的路径,参数mode是制作管道文件的权限。返回值为int类型,创建成功返回0,创建失败返回-1。
注:使用mkfifo接口需要包含<sts/types.h>和<sys/stat.h>头文件。
命名管道接口函数方式通信的简单实现:
创建clientFifo.cpp和serverFifo.cpp两个文件,写入下图一二所示的代码。创建comm.h头文件,写入下图三所示的代码。创建makefile文件,写入下图四所示的代码。使用make命令生成clientFifo和serverFifo两个可执行程序,先使用./serverFifo命令运行serverFifo可执行程序(因为命名管道文件是在serverFifo进程中创建),然后使用./clientFifo命令运行clientFifo可执行程序。在clientFifo进程中输入内容,内容通过命名管道传输到serverFifo进程中然后被打印出来,如下图五所示。
在comm.h头文件中定义一个路径IPC_PATH,以供两个进程创建和打开管道文件时锁定同一路径。命名管道文件只需要mkfifo创建一次,这里在serverFifo进程中创建,然后两个进程打开创建好的命名管道文件即可进行通信。

注:

1.makefile文件中gcc生成对应可执行程序的Wall选项,其作用是编译时显示出所有的告警信息。
2.clientFifo进程中输入内容,输入完之后回车的时候会追加一个/n,因此serverFifo进程中将传输内容打印出来的时候后面会多一个/n,表现出来是空一行。
我们在clientFifo进程中使用write向命名管道写入内容之前,使用line[strlen(line) - 1] = '\0'代码将后面追加的/n用/0替代即可(fgets函数是c语言函数读取cin中内容时会自动在结尾处加一个/0,因此line中下标strlen(line)对应fgets函数自动添加的/0,line中下标strlen(line)-1才对应回车的时候追加的/n)。
3.如果在clientFifo进程中输入结束,使用ctrl+d停止输入即可。
4.命名管道文件的删除使用unlink接口函数,其声明如下图所示,参数pathname是删除命名管道文件的路径。

3.System V IPC

3.1.system V共享内存

共享内存概念:

前面讲过,不同的进程其进程地址空间通过页表映射到物理内存的不同区域。共享内存就是操作系统在物理内存中开辟一块空间,通过不同进程的页表映射到不同进程对应进程地址空间的共享区部分,然后将映射到进程地址空间共享区的共享内存地址返回给用户(共享内存通过页表映射到不同进程的进程地址空间共享区部分位置可能不同,因此不同进程返回给用户的共享内存地址也可能不同)。

从上面的概念就可以得出,要使用共享内存需要经过两个步骤:

1.在物理内存中创建和删除共享内存。

2.不同的进程关联和去关联共享内存。

补充:

操作系统要将内存中的一个个共享内存管理起来,要管理就要先描述再组织,Linux内核中提供了描述共享内存的shmid_ds结构体,如下图所示,在shmid_ds结构体中有一个ipc_peim结构体类型的成员变量,ipc_peim结构体如下图二所示,其中mode成员变量保存的是共享内存的权限,key_t类型的__key成员变量用来表示共享内存的唯一值(key_t类型其实就是int整型),用来保证共享内存的唯一性,__key值由调用shmget函数创建共享内存时用户来提供(shmget函数下面会讲)。

问题:用来保证共享内存唯一性的__key成员变量,在使用shmget函数创建共享内存时作为参数key由用户来提供。为什么这个保证共享内存唯一性的key值要由用户提供呢?

答:在内核中,要让不同的进程看到同一份共享内存,做法是让不同的进程拥有同一个共享内存的key值即可。如果key值由操作系统统一提供,那么调用shmget函数创建共享内存时返回key值,返回的key值只有本进程知道,其他进程无法知道创建的共享内存的key值,如果创建共享内存时由用户提供key值,那么相当于所有进程都知道对应共享内存的key值。

注:

1.共享内存的key值与命名管道的文件名类似,命名管道不同的进程之间约定使用同一个文件名的管道,共享内存不同的进程之间约定好使用同一个key值。

2.key值的大小虽然是用户设定的,但也不能随便的设定key值,通常我们使用ftok函数帮助我们生成对应的key值,由ftok函数生成的key值具有唯一性,如下图所示,pathname参数是文件路径,proj_id参数由自己设定,一般是0-255之间的值。如果ftok函数调用成功则返回对应的key值,如果ftok函数调用失败则返回-1。

使用ftok函数需要包含<sys/types.h>和<sys/ipc.h>头文件。

共享内存的接口函数:

shmget函数:

功能:用来创建共享内存
参数:
key: 这个共享内存段名字
size: 共享内存大小
shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的 mode 模式标志是一样的
返回值:
成功返回一个非负整数,即该共享内存段的标识码shmid(key值是在内核中标识共享内存的,标识码shmid是在用户层标识共享内存的),失败返回-1。

注:

1.参数size即共享内存大小建议设置为页(4KB)的整数倍。

4GB的内存空间被分为1048576页,而1048576页就是2^{20}页,操作系统要将这2^{20}页内存空间管理起来就要先描述再组织,在Linux内核中有结构体page来描述一个页,有struct page mem[2^{20}]数组来将一个个页组织起来,那么操作系统对页做管理就变成了对struct page mem[2^{20}]数组做管理,进而实现了将这2^{20}页内存空间管理起来。

从这里可以看出操作系统将内存空间划分为了一个个page进行管理,因此在申请内存空间时最好申请的大小为page页的整数倍。

如果这里参数size不是页(4KB)的整数倍,那么即使查询时显示共享内存大小是这里size值即用户设定的值,使用时共享内存大小也确实是这里的size值即用户设定的大小,但实际操作系统向内存申请空间时是向上取整,申请整数倍页空间。

2. 在进程调用shmget函数创建共享内存时,共享内存此时可能没有被创建,也可能已经被创建了。shmget函数的参数shmflg有两个宏选项,分别为IPC_CREAT和IPC_EXCL,IPC_CREAT是创建共享内存,如果共享内存已经存在就获取之,如果共享内存不存在就创建之。

IPC_EXCL不单独使用,必须和IPC_CREAT配合使用(即 IPC_CREAT | IPC_EXCL),如果共享内存不存在就创建之,如果共享内存已经存在就出错返回,即shmget函数返回-1并且设置errno。这里IPC_EXCL和IPC_CREAT配合使用的意义为可以保证如果shmget函数调用成功,则一定是一个全新的共享内存。

如果shmget函数的参数shmflg设置为0,则默认是IPC_CREAT选项。

如果参数shmflg只是IPC_CREAT或IPC_CREAT|IPC_EXCL,那么创建出来的共享内存权限为0,其他进程无法访问,因此shmflg参数中还需要再 | 上权限值,例如 IPC_CREAT|0666 、IPC_CREAT|IPC_EXCL|0666 等。

3.使用shmget函数需要包含<sys/ipc.h>和<sys/shm.h>头文件。

shmctl函数:

功能:用于控制共享内存

参数:

shmid: shmget 返回的共享内存标识码
cmd: 将要采取的动作(有下图所示的三个可取值)
buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构(如果cmd为IPC_RMID删除共享内存,buf参数设为NULL即可)
返回值:
成功返回 0 ,失败返回 -1。

注:使用shmctl函数需要包含<sys/ipc.h>和<sys/shm.h>头文件。

shmat函数:

功能:将共享内存段连接到进程地址空间

参数:

shmid: 共享内存标识
shmaddr:指定连接映射到进程地址空间的共享内存的地址(一般设置为NULL即可,系统会自动选择一个合适的地址)
shmflg: 它的两个可能取值是 SHM_RND(以读写方式连接) 和SHM_RDONLY(以只读方式连接)(一般设置为0,默认0是SHM_RND,即读写方式连接)
返回值:
成功返回连接映射到进程地址空间的共享内存的地址,失败返回 -1。

注:

1.使用shmat函数需要包含<sys/types.h>和<sys/shm.h>头文件。

2.shmat函数返回的共享内存地址与malloc返回的申请堆空间的地址类似,malloc返回的堆空间如何使用,shmat返回的共享内存空间就如何使用。

3.经过shmat函数的连接,就将共享内存映射到了我们的进程地址空间中(映射到了用户空间了)(堆和栈之间),对每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或栈空间,可以被用户直接使用,不需要调用系统接口。

shmdt函数:

功能:将共享内存段与当前进程脱离

参数:

shmaddr: 由shmat所返回的地址,即连接映射到进程地址空间的共享内存的地址。
返回值:
成功返回 0 ,失败返回 -1。
注:
1.将共享内存段与当前进程脱离不等于删除共享内存段。
2.使用shmdt函数需要包含<sys/types.h>和<sys/shm.h>头文件。

共享内存接口函数方式通信的简单实现1:

创建IpcShmCli.cc和IpcShmSer.cc两个文件,写入下图一二所示的代码。创建comm.hpp文件,写入下图三所示的代码。创建Log.hpp文件,写入下图四所示的代码。创建makefile文件,写入下图五所示的代码。使用make命令生成IpcShmCli和IpcShmSer两个可执行程序,先使用./IpcShmSer命令运行IpcShmSer可执行程序(因为共享内存是在IpcShmSer进程中创建),此时IpcShmSer进程已经在读取共享内存并进行打印,然后使用./IpcShmCli命令运行IpcShmCli可执行程序,IpcShmCli进程不断往共享内存中写入内容,此时IpcShmSer进程读取到的就是IpcShmCli进程不断写入的内容,如下图六所示。
comm.hpp文件中定义了一个路径IPC_PATH和一个ID值PROJ_ID,路径IPC_PATH和ID值PROJ_ID作为ftok函数的参数生成共享内存的key值,定义CreateKey函数其调用ftok函数生成共享内存的key值,定义MEM_SIZE作为开辟共享内存的大小。Log.hpp文件中定义了Log函数用来辅助打印内容。

注:

1.进程退出则进程所打开的文件会自动关闭(如果该文件有其他进程打开则文件struct file的引用计数减一),进程退出则进程所创建的共享内存不会自动释放,共享内存依然存在,这是因为共享内存虽然是被某个进程创建的,但其不属于某个进程,而是属于操作系统的。system V下的共享内存,其生命周期是随内核的,如果不显式的删除释放,那么只能通过kernel操作系统重启来删除释放。

使用ipcs -m命令可以查看当前用户创建的共享内存,如下图所示。key是共享内存key值(内核层面),shmid是共享内存的标识码(用户层面),owner是共享内存拥有者(用户),perms是共享内存的权限,bytes是共享内存的大小,nattch是连接共享内存的进程数。

2.删除共享内存的接口函数为shmctl函数,除了该函数也可以使用 ipcrm -m 标识码shmid命令来删除共享内存,如下图所示。

3.共享内存是由哪个进程创建的就由哪个进程删除释放,这里共享内存不是由IpcShmCli进程创建的,因此IpcShmCli进程不进行删除释放。

4.这里IpcShmSer进程在死循环读取内容,我们只能通过ctrl c的方式结束进程,通过这种方式结束IpcShmSer进程,IpcShmSer进程没有执行shmctl函数关闭共享内存,因此结束进程之后还需要 ipcs -m 查找对应共享内存标识码,并且 ipcrm -m 标识码 手动关闭对应的共享内存。

共享内存接口函数方式通信的简单实现2:

我们修改简单实现1中的IpcShmCli.cc、IpcShmSer.cc、comm.hpp文件,如下图一二三所示,Log.hpp和makefile文件不变,如下图四五所示。

修改后,在IpcShmCli进程中通过键盘写入内容,将写入内容通过共享内存传输给IpcShmSer进程,IpcShmSer进程将内容打印到显示器上。

使用make命令生成IpcShmCli和IpcShmSer两个可执行程序,先使用./IpcShmSer命令运行IpcShmSer可执行程序,此时IpcShmSer进程已经在读取共享内存并进行打印,然后使用./IpcShmCli命令运行IpcShmCli可执行程序,IpcShmCli进程不断将键盘中输入的内容写入到共享内存中,此时IpcShmSer进程读取到的就是不断从键盘中写入的内容,如下图六所示。

共享内存的特点:

\bullet 没有访问控制、不安全。 

共享内存因为其自身的特性,其没有任何访问控制,共享内存被双方进程之间看到,属于双方的用户空间,可以直接通信,但是不安全。

\bullet 共享内存是所有进程间通信速度最快的。 

如果是进程1和进程2使用管道通信,那么外设键盘到进程1拷贝一次,进程1到管道拷贝两次,管道到进程2拷贝三次,进程2到外设显示器拷贝四次。如果是进程1和进程2使用共享内存通信,那么外设键盘到进程1的共享内存拷贝一次,进程2的共享内存到外设显示器拷贝两次。

共享内存接口函数方式通信的简单实现3:

因为共享内存没有访问控制或同步机制,而简单实现1和简单实现2的IpcShmSer进程端是每隔一秒读取共享内存一次并打印到显示器上,因此打印出来的内容不规范。我们可以通过命名管道进行控制,实现基于共享内存+管道的一个访问控制效果。

在IpcShmSer进程中创建命名管道,分别在IpcShmSer进程和IpcShmCli进程中打开命名管道。在IpcShmSer进程中通过read函数读取管道内发送的信号,如果IpcShmCli进程没有发送信号就阻塞式等待,当IpcShmCli进程发送信号了就从共享内存中读取内容并打印,如果read读取信号返回0那么说明IpcShmCli进程将管道关闭并且管道中信号内容已读取完,此时IpcShmSer进程不再从共享内存中读取。分别在IpcShmSer进程和IpcShmCli进程结束时关闭命名管道。

我们修改简单实现2中的IpcShmCli.cc、IpcShmSer.cc、comm.hpp文件,如下图一二三所示,Log.hpp和makefile文件不变,如下图四五所示,使用make命令生成IpcShmCli和IpcShmSer两个可执行程序,先使用./IpcShmSer命令运行IpcShmSer可执行程序,此时IpcShmSer进程等待IpcShmCli进程通过管道传输信号,然后使用./IpcShmCli命令运行IpcShmCli可执行程序,当从键盘输入内容后,IpcShmCli进程将内容写入到共享内存中,然后通过管道向IpcShmSer进程发送读取信号,IpcShmSer进程收到读取信号后将写入到共享内存的内容读取并打印出来,如下图六所示。

comm.hpp文件中定义了CreateFifo函数用来创建命名管道文件,定义了Open函数用来以参数flags的方式打开对应filename文件,定义了Wait函数用来读取指令信息,定义了Siganl函数用来发送指令信息,定义了Close函数用来关闭filename文件。

3.2.system V消息队列(选学了解)

消息队列:消息队列的本质是由Linux内核创建用于存放消息的链表,并且其功能是用来存放消息的,所以又称之为消息队列。

在Linux的不同进程中,包括有血缘的进程和无血缘的进程,都可以通过Linux消息队列API所得到的消息队列唯一标识符对消息队列进行操作。

注:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

system V消息队列实际中使用较少,消息队列的一系列操作函数这里不再进行讲解。

3.3.system V信号量(选学了解)

补充知识:

\bullet 临界资源:能够被多个进程同时看到的资源被称为临界资源(管道、共享内存都是临界资源),如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候就都是乱序的,可能会因为读写交叉而导致乱码、废弃数据等访问控制方面的问题。

\bullet 临界区:对多个进程而言,访问临界资源的代码称为临界区。我们的进程中有大量代码,只有访问临界资源的那部分代码叫做临界区。 

\bullet 原子性:我们把一件事情要么没做要么做完了(即没有中间状态)称作原子性。

\bullet 互斥:任何时刻,只允许一个进程访问临界资源,我们称为互斥。

信号量的概念:

临界资源要对进程的访问进行限制。首先,临界资源要保证访问的进程数不能多余一个临界值,其次,临界资源要保证可访问的进程访问的是临界资源的不同资源块。

通过使用一个计数器cnt来控制临界资源要访问的进程数少于一个临界值,我们将计数器cnt的初始值设为该临界值,每当有一个进程访问时cnt--,每当有一个进程不再访问时cnt++,当cnt为0时如果再有进程要访问就需要进行等待。这里的计数器cnt我们一般称为信号量,信号量的本质就是一个计数器。

任何进程想访问临界资源,就必须先申请信号量,如果申请成功,就能够访问临界资源中的一部分资源。

注:

1.当临界资源的信号量为1时,表现的就是互斥特性。具有互斥特点的信号量称为二元信号量(要么为0要么为1),常规的信号量称为多元信号量。

2.信号量是一个计数器,这个计数器对应的操作(++、--等)是原子的(要么做完要么没做玩,没有中间状态)。

如果信号量不是原子的,那么信号量--的过程首先需要CPU读取变量,然后--操作,最后把--的结果返回给信号量,那么一个进程访问临界资源信号量--时,CPU执行--操作后准备将结果99返回给信号量时,CPU切换到其他进程了,其他进程此时看到的信号量仍然是100,然后其他进程都访问临界资源使得信号量减为50,此时CPU再次切换到之前的进程,将99返回给信号量,信号量被赋值为99,出现错误,因此信号量必须是原子的。

信号量的--操作对应申请信号量资源,该操作称为P操作,信号量的++操作对应释放信号量资源,该操作称为V操作,因此与信号量匹配的操作是PV操作,PV操作是原子的。

3.共享内存没有访问控制,可以通过信号量对共享内存进行保护。

4.IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

system V信号量实际中使用较少,信号量的一系列操作函数这里不再进行讲解。

3.4.System V IPC补充知识

所有的System V IPC资源(共享内存、消息队列、信号量)在Linux内核中被ipc_id_ary数组所管理。

前面我们学过了Linux内核中描述共享内存的shmid_ds结构体,内核中还存在描述消息队列的semid_ds结构体和描述信号量的msqid_ds结构体,如下图一二三所示,操作系统只要能找到对应共享内存/消息队列/信号量的对应结构体就能对其进行管理。这三个结构体有一个共同点就是其第一个成员变量都是struct ipc_perm类型的结构体成员变量。

ipc_id_ary数组是一个指针数组,其包括类型写全为struct ipc_perm * ipc_id_ary[N],ipc_id_ary数组里面所有的元素都指向一个struct ipc_perm类型的结构体,即每种System V IPC资源的第一个成员变量字段,如下图四所示。

因为结构体的地址与其第一个成员变量的地址相同,ipc_id_ary数组中想要管理某一个System V IPC资源,在数组内部找到该资源的ipc_perm类型成员变量地址,然后(struct shmid_ds/semid_ds/msqid_ds)ipc_id_ary[n]将指针强转为对应的结构体类型就可以管理访问对应的System V IPC资源了。

这样就可以通过统一的规则,在内核中管理不同种类的IPC资源了。

注:前面学过的共享内存的shmid值就是对应共享内存的shmid_ds结构体第一个ipc_perm类型的成员变量地址存储在ipc_id_ary数组的下标值,操作系统通过ipc_id_ary数组对应的shmid下标经过强制类型转换就可以访问对应共享内存的shmid_ds结构体,也就可以管理对应的共享内存了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值