在上一篇博客中,我们已经熟悉并使用了匿名管道,这篇博客我们将讲述进程间通信另外两种常见方式——命名管道与共享内存。
1.命名管道
管道是使用文件的方式,进行进程之间的通信。因此对于管道的操作,实际上还是用诸如write,read等接口实现。
匿名管道应用的一个限制就是只能在具有亲缘关系(如父进程与子进程、兄弟进程)之间进行通信。如果想在不相关的进程间进行数据交换,可以使用FIFO文件来做这种工作。
这里的FIFO文件即我们所说的命名管道。必须强调的是,虽然FIFO是一种文件,但实际上数据的读写都是在操作系统开辟的内存缓冲区中进行的,并不会真的写入磁盘中。如果那样,进程间通信的效率将会极大降低!
1.1 创建命名管道
1.1.1 使用命令行创建
在命令行中使用mkfifo 管道名
的方式来创建命名管道。如下图
可以看到文件类型为p,即管道类型的文件。文件大小为0,即便写入到pipe中的数据没有被另一个进程读出,依然是0!因为根本不会将数据写入到磁盘中。
下例中我们将hello world重定向到pipe中,并且从pipe中读出数据显示到屏幕。使用两个shell,进入到同一个目录下,一个在命令行输入echo hello world >pipe
,另一个输入 cat < pipe
, 可以看到在另一个shell的屏幕上出现了hello world。
1.1.2 使用接口创建
在命令行输入man 3 mkfifo
后可以看到如下内容:
该接口一共有两个参数,其中第一个创建的fifo文件的路径,第二个是文件权限,与我们之前学习的文件操作的权限一模一样。返回值如果等于0则创建成功,-1则创建失败。
1.2 匿名管道和命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。其余操方式与文件的操作没有区别。
2. system V共享内存
2.1 什么是共享内存?
共享内存是最快的IPC形式,是系统在内存中开辟一块内存,用于进程之间共享地进行操作。这片内存区域经过页表映射到通信进程各自的地址空间中,不同的进程可以通过操作自己的地址空间,来操作共享内存。如下图所示:
2.2 共享内存函数
共享内存的创建及销毁一般分为以下几步:
- 操作系统在内存中申请一片内存
- 将共享内存挂接到进程地址空间中
- 使用完毕后,将共享内存和进程地址空间去关联
- 操作系统销毁共享内存
因此,操作系统提供了以下几组接口:
2.2.1 ftok函数
key_t ftok(const char *pathname, int proj_id);
该函数有两个参数,第一个是路径名称,第二个是项目id。两者配合用于创建唯一的key值,该值可以在系统内标定唯一的通信资源。(为什么这里不说标定唯一的共享内存,是因为system v包含共享内存,消息队列,信号量三种通信方式。三种通信方式都是使用ftok函数创建唯一的key来标定唯一性的。)
2.2.2 shmget函数
int shmget(key_t key, size_t size, int shmflg);
该函数用来创建共享内存。有三个参数:
key即系统层面上这个共享内存段名字,就是我们用ftok获取的唯一值。
size是共享内存大小,一般是页的整数倍。(一个page是4096 bytes
shmflg是由九个权限标志构成的,它们的用法和创建文件时使用的mode模式标志是一样的。IPC_CREATE 的用法是如果key标定的共享内存不存在则创建,若存在则直接使用。IPC_EXCL一般要配合IPC_CREATE使用,即key标定的共享内存不存在则创建,若存在则报错,常用于申请开辟共享内存的一端。除此之外,在创建共享内存时,还应该标定该共享内存的权限(与文件中设置权限如出一辙),如0664等。
返回值:成功返回一个非负整数,即该共享内存段的标识码(注意,该标识码是用户层面的标识码,简单,方便用户使用!而key是系统层面的!;失败返回-1。)
2.2.3 shmat函数
void* shmat(int shmid, const void* shmaddr, int shmflg);
该函数用于将共享内存挂接到进程地址空间中。
shmid参数是使用shmget获取的用户层面的共享内存标识符。
shmaddr参数是挂接到进程地址空间中的起始虚拟地址,用于一般不关心,设置为NULL,让操作系统自己选择分配。
shmflg参数:0即对该共享内存有读写权限,另一个SHM_RDONLY即对该共享内存有只读权限。
返回值:成功返回一个指针,指向共享内存起始地址;失败返回-1。
2.2.4 shmdt函数
int shmdt (const void* shmaddr);
该函数用于将共享内存与当前进程去关联。
唯一的一个参数shmaddr是由shmat所返回的指针。
去关联成功则返回0,失败则返回-1。
注意:去关联与销毁共享内存是完全不同的两个概念!
2.2.5 shmctl函数
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
功能:用于控制共享内存(常用于销毁共享内存)
第一个参数shmid是shmget函数返回的共享内存标识符,cmd有三个可以采取的动作,常使用IPC_RMID来销毁共享内存,buf指向一个保存着共享内存的模式状态和访问权的数据结构,如果是为了销毁该共享内存,直接置为NULL。
成功返回0,失败返回-1。
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
是system v的三种通信方式共有的,该结构体内就有key值。
unsigned short shm_nattch;
这个字段即挂接该共享内存的进程数。
4.使用共享内存的例子
4.1实例代码
Makefile:
.PHONY: all
all: server client
client: client.c comm.c
gcc -o $@ $^
server: server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server
comm.h
#ifndef COMM_H
#define COMM_H
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0x66
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif
comm.c
#include "comm.h"
static int commShm(int size, int flags)
{
key_t _key = ftok(PATHNAME, PROJ_ID);
if(_key < 0)
{
perror("ftok");
return -1;
}
int shmid = shmget(_key, size, flags);
if(shmid < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT|IPC_EXCL|0666);
}
//creatShm是server端创建shm,getShm是client端获取创建好的shm
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}
server.c
#include "comm.h"
int main()
{
int shmid = createShm(4096);
char* addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
while(1)
{
printf("client# %s\n",addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}
client.c
#include "comm.h"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(1)
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
4.2 效果展示
在./server运行之前,在命令行输入ipcs -m命令,查看此时共享内存情况。发现系统中此时没有共享内存。
./server运行后,此时可以看到nattach变为1,即有一个进程挂接到该共享内存。
运行./client后,此时nattch变为2,即此时有两个进程挂接到该共享内存。
在./client端ctrl+C之后,发现./server端还在打印,此时打印的数据不再改变,因为没有进程向共享内存中写入数据。此时nattch变为1。
在关闭./server之后,发现nattch变为1。但是,标识符为5的共享内存仍然存在!!也就是说,共享内存的生命周期随内核!!而不是随进程!!
4.3 共享内存的特性
通过4.2中的例子,我们可以得到共享内存的以下特性:
1.共享内存的生命周期随内存。
2.系统层面是用key来标识共享内存的,而用户层面是通过shmid来进行标识,且shmid比key要简单得多。
3.可以使用ipcs -m
命令来查看共享内存的状态,使用ipcrm -m +shmid
来删除共享内存。
4.删除共享内存是销毁内存中的内存空间,而去关联实际上是删除进程页表中进程地址空间和对应共享内存的映射关系。
5.如果在删除的时候,nattch字段为0,那么内核中描述共享内存的结构体也被释放了。如果在删除的时候,nattch字段不为0,那么key会变为0x00000000.表示当前共享内存不能被其他进程挂接,共享内存的status变为destroy。如下图所示:在client运行过程中使用ipcrm -m 6删除6号共享内存,再使用ipcs -m查看,此时key为0x00000000,status为dest。
在共享内存status为dest时,一旦该共享内存的进程挂接数为0,共享内存将会被立即销毁。如下图所示。在./client被crtl C后,使用ipcs -m命令,此时系统中再无共享内存。
6.共享内存不提供同步与互斥机制(回想一下,管道是否提供)。
7.共享内存是最快的进程通信方式。因为共享内存写是覆盖写的方式,读是直接访问地址。而我们之前所学习的管道,需要通过系统调用(如write,read)来进行数据的读写,相比之下,共享内存的数据拷贝次数更少,因此效率也会更高。
3.其他进程通信方式
除了共享内存,system v还提供了消息队列和信号量两种进程通信方式,其中消息队列还是为了实现进程间的数据交换,而信号量主要是用于实现进程之间的同步与互斥机制。
3.1 system V消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。
3.2 system V信号量
信号量主要用于同步和互斥的。
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。在进程中涉及到互斥资源的程序段叫临界区。
关于同步与互斥,我们将在多线程部分重点学习。