目录
进程间通信简介
进程间拥有相互独立的内存空间,包括有血缘关系的父子进程,那么进程之间的通信就需要用到通信工具(IPC),主要分为两类:数据传输工具和共享内存。常用的通信工具如下:
数据传输工具:一个进程将数据写入到IPC中(进程内存到内核内存),另一个数据从IPC中读取数据 (内核内存到进程内存)。
共享内存:其中一个进程可以将数据放入到共享内存,其它进程访问该内存读取数据,该方式无需调用进程内存和内核内存,所以速度较快。
管道
简介和特征
管道用于有血缘关系的进程中,如图所示,两个进程连接到了管道上,写进程通过文件描述符1连接到了管道的写入端,读进程通过文件描述符0连接到读取端,他们只是从文件描述符中读取和写入数据。
管道有如下特征:
- 一个管道是一个字节流,从管道读取数据的进程可以读取任意大小的数据块,并且读取字节的顺序和写入的顺序是一致的。
- 管道是单向的,一端写入一端读取,所以如一个线程作为读取端时要将写入端关闭,当读取管道所有数据后会看到文件结束,如果不关闭该线程写入端如果读取完数据它会一直阻塞到有数据被写入管道,因为作为读取端的线程不关闭写入,理论上也可以作为写入端向缓冲区写数据。
- 管道的容量是有限的,存储能力为65536字节,管道填满后,向管道的写入操作会被阻塞直到从管道移除一些数据
常用函数
创建管道函数:pipe()
成功调用pipe()后,直接使用read()和write()来从管道读写数据,写入端写入数据之后,读取端可以立马读取。一般使用管道让两个进程进行通信,调用pipe函数后,再调用fork()函数创建子进程,子进程会复制父进程的内容,这个时候父子进程的两个文件描述符fileds[0]和fileds[1]都是打开的,再根据通信需要关闭写入端和读取端,如下图所示。如果要兼具父子进程均有读写功能,则需要用两根管道。
举例
如下程序通过fork()函数创建了父子进程,然后通过close函数父进程关闭读取端,子进程关闭写入端,父进程通过write函数向管道写入数据,子进程通过read函数从管道读取数据。
现象:父线程在延迟10s后向管道写入“Hello world!”,子进程先打印提示信息,然后再过10秒打印从管道读取到的信息。
FIFO(有名管道)
简介和特征
父子间的进程或者有血缘关系的进程之间的通信可以使用管道来完成,但两个无血缘关系之间的进程进行通信无法使用管道来完成,这时就需要用到FIFO。
FIFIO特征:
- 和管道类似,两者最大的区别在于FIFO在文件系统中拥有一个名称,打开方式和普通文件一样。其也有一个读取端和输入端。
- 可以在非血缘关系进程中进行通信。
常用函数
创建管道函数:mkfifo()
通过mkfifo函数建立一个名为pathname管道,它是一个文件,而mode参数指定了该文件的权限。在使用FIFO时,一般设置一个读进程和写进程。
举例
如下程序,各建立了一个读写的程序,二者相互独立,在读程序中用mkfifo函数建立了My_fifo管道,而在写程序中可以直接调用,分别打开两个终端,先运行写程序,程序会阻塞到“Prepare…”那句,然后再在另一终端打开写程序,输入参数,向管道发送数据,同时,读程序读管道,结束程序。
遇到的问题:
- 关于调用mkfifo函数创建一个文件My_fifo后,如程序有修改当你再次编译时,这个时候就会报错,因为My_fifo文件已经存在了,可以添加判断文件是否已经存在的代码来解决。
- 关于权限问题,mkfifo函数的第二个参数是这个文件的属性,即使设置的是777,即可读写,但是如果没有切换到root用户,My_fifo也无法使用,写程序会提示“Permission denied”即权限不够。
- 关于阻塞,如果先执行读程序,会阻塞到写程序打开并写入数据至FIFO,先执行写程序同理。
共享内存
简介和特征
共享内存允许两个或多个进程共享物理内存的同一块区域(段),其中一个进程将数据复制到共享内存中,这部分数据对其它共享该段的进程可用,与管道和消息队列相比,这种速度更快。
常用函数
使用共享内存步骤
- 调用shmget()创建一个共享内存段,返回得到共享内存标识符。
- 调用shmat()使该段成为调用进程的虚拟内存的一部分。即在进程中映射到共享内存。
- 当一个进程不在需要访问共享内存的时候,可以调用shmdt()解除映射关系。
- 调用shmctl()删除共享内存段。
创建共享内存函数:shmget()
该函数返回一个共享内存标识符,而第一个参数key是一个键值,用以在内核中标识该共享内存标识符,二者一一对应,一般设置为IPC_PRIVATE,第二个参数size为共享内存的大小,第三个参数类似于一个标志位,一般设置为IPC_CREAT,配置好三个参数就会生成一块size大小的共享内存。具体如下:
映射共享内存函数:shmat()
创建好共享内存后,需要用shmat函数在进程中映射到共享内存,该函数第一个参数是共享内存标识符,第二个参数是一个地址用以指定存放映射的位置,一般设置为NULL,内核自动映射到一个合适的地址;第三个参数用以设置映射过来空间的权限,如果不配置该参数即0有读写权限。映射成功后,函数返回映射到当前进程空间的首地址。
解除映射函数:shmdt()
当一个进程不在需要访问一个共享内存时,可以用shmdt函数解除映射关系。函数参数就是shmat函数的返回值。
删除共享内存函数:shmctl()
创建共享内存自然需要销毁创建的共享内存操作,销毁用到shmctl函数,第一个参数为创建的共享内存的标识符,即shmid,第二个参数cmd为待执行的操作,销毁用IPC_RMID,第三个参数buf在其他待操作时才设置,销毁时用NULL。
举例
父子进程通过共享内存通信
该程序申请了一块1024字节的共享内存,父进程通过内存映射向共享内存中写入字符串,子进程通过内存映射读取该字符串。
- 出现的问题:该代码我仿照老师写完之后运行报Segmentation fault (core dumped)错误,找了很久没找出问题,甚至将老师的源码下载运行发现还是报该错误,最后发现切换到root下运行就没问题。
- 解决:我们程序中使用shmget()函数,最后一个形参flag,我们用了IPC_CREAT,而并没有对其进行权限设置,我们只需要在其或上权限标识即可。即shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)。
不相关进程通过共享内存通信
非亲缘关系下如何用共享内存进行通信,我们在使用int shmget(key_t key, size_t size, int shmflg)函数时,第一个参数是创建共享内存在内核标识下的一个键值,我们前面创建时用默认的宏定义IPC_PRIVATE,系统自己匹配一个键值。当两个不相关的进程调用shmget()函数时,如果也使用默认设置,那两个进程的键值显然不一样,即他们映射的就不是同一块共享内存,自然无法完成通信,所以我们只需要在两个进程调用shmget函数时使用我们设置的相同的一个键值,即可完成通信,可以通过宏定义自己定义一个键值,为了避免与其它键值冲突也可以使用fotk函数创建一个。
如下程序所示,创建了一个读程序和一个写程序,同时键值都设置成9527,写程序向共享内存写入“hello world”,读程序顺利从共享内存读出“hello world”。
消息队列
简介和特征
用管道发送数据时,管道内的数据是无法进行区分的字节流,可以读取任意数量的字节,不用管发送端向管道发送了多大的数据块。而消息队列接受者接收到的是发送者的整条消息。
常用函数
创建消息队列函数:msgget()
这个函数同创建共享内存shmget()函数,函数返回一个消息对列标识符,第一个参数同shmget()函数是一个键值,一般也是IPC_PRIVATE或者ftok()返回的一个键,也可以自己宏定义或者fort()一个键值,同上共享内存的例子。msgflg是一个指定施加于新消息队列上的权限和检查一个既有队列权限的掩码,一般同shmget()用IPC_CREAT创建一个消息队列,同时也可以或上权限标识。
交换消息函数:msgsnd()和msgrcv()
上面是发送消息和接收消息的函数,第一个参数是消息队列标识符,第二个是自己定义的一个结构体指针。结构体第一个参数是自己定义的一个消息类型,用long的整数来表示,剩余的参数就是我们要发消息的内容,根据需要自行定义。
msgsnd()第三个参数是要发消息的大小,第四个参数是标记的位掩码,一般不设置默认为0.
msgrcv()第三个参数是接收缓冲区的大小,第四个参数为选择消息,因为读消息顺序可以不和发消息顺序一致,所以通过该参数选择我们要读哪种消息,就是我们发消息中的mtype,也可以设置为0代表队列中的第一条消息。第五个参数标志位同上。
删除消息队列函数msgctl()
删除消息队列函数同共享内存删除函数。
举例
父子进程通过消息队列进行通信
创建消息队列后,父进程等待3秒后,从键盘中读取信息后,传到消息队列中,子进程同步进行先打印提示信息,然后进入阻塞,3秒后从队列中读取到信息并打印。
不相关进程通过消息队列进行通信
不相关进程通过消息队列通信和上述共享内存通信一样,我们先自己定义一个键值,其余同父子进程通信基本一致。