《UNIX环境高级编程》笔记——进程间通信

之前对进程间通信(InterProcess Communication,IPC)的了解较少,因此阅读《UNIX环境高级编程》相关章节,系统学习一下进程间通信,做点记录。

1 管道

管道通过pipe函数创建

#include<unistd.h>
int pipe(int fd[2]);
//成功时返回0,失败返回-1

fd返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开,fd[1]写入的正是fd[0]读的。可见这两个文件描述符就像是管道的两个口子,进程间可以通过这两个口子进行通信,数据通过内核在管道中流动。

注意使用管道传输数据为了避免出错,需要把发送方进程的读文件描述符关闭,把接收方进程的写文件描述符关闭,这样就不会干扰对方使用的文件描述符。因此如果只用一个管道,则理论上只能半双工通信,比如父进程传输数据给子进程。
在这里插入图片描述
如果要全双工通信,可以创建两个管道。
(画得很草)
在这里插入图片描述

示例:
父进程输出两个数,子进程求和(还没写)


2 FIFO

FIFO,即命名管道。

未命名的管道只能在两个相关进程之间使用,而且这两个相关的进程还要有一个共同的创建了他们的祖先进程。但是,通过FIFIO,不相关的进程也能交换数据。

FIFO是一种文件类型,创建FIFO类似与创建文件。

#include<sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
//两个函数都是成功时返回0,失败返回-1
  • path:要创建的文件名
  • mode:指定文件的访问权限
  • fd:文件描述符

相较于mkfifo,mkfifoat可用来在fd文件描述符表示的目录相关的位置创建一个FIFO。
用mkfifo或mkfifoat创建FIFO时,要用open来打开它。通过设置O_NONBLOCK来指定非阻塞,只读open会立即返回,若没有进程为读而打开FIFO,只写open将返回-1。若未指定O_NONBLOCK,则只读open将阻塞到其他进程为写而打开对应的FIFO位置,只写open阻塞到某个进程为读而打开FIFO。

示例:
两个独立的进程简单通信。(还没写)


3 XSI IPC

XSI IPC包括消息队列、信号量和共享存储器。它们之间有很多相似的特性。
两个概念:标识符和键

标识符(identifier):每个内核中的IPC结构(消息队列、信号量和共享存储段)都用一个非负整数的标识符加以引用。

键(key):标识符是IPC对象的内部名,为使多个进程能够在同一IPC上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个(key)相关联,这个键作为该对象的外部名。

每个内核IPC对象都用对应的get函数(msgget, semget, shmget)创建,这类IPC对象时通过指定让对应的IPC对象与之关联,并生成标识符,即IPC的id号。

3个get函数msgget,semget,shmget分别用于创建消息队列、信号量和共享存储器。这三个函数都有两个类似的参数:key、flag。

  • key:键值,设为IPC_PRIVATE或者与当前某种类型的IPC结构无关,则可创建一个新的对象并返回其 id号,此时要指明flag的IPC_CREAT标志位。引用现有的IPC对象事key值需要等于该对象创建时指明的key值,此时flag的IPC_CREAT标志位无需指明。
  • flag:标志位,创建新IPC内核对象要指定IPC_CREAT,并要确保没有引用同一标识符的现有IPC,要指定IPC_EXCL,用现有的则不被指明。创建新的对象,flags还需要位或内核对象的权限位。

内核为每个IPC关联一个ipc_perm结构,规定权限和所有者,结构如下:

struct ipc_perm {
    uid_t          uid;      //所有者有效用户id
    gid_t          gid;      //所有者有效用户组id
    uid_t          cuid;     //创建者有效用户id
    gid_t          cgid;     //创建者有效用户组id
    unsigned short mode;     //权限位
};

IPC权限:

权限
用户读0400
用户写(更改)0200
组读0040
组写(更改)0020
其他读0004
其他写(更改)0002

使用get函数示例:

int id = msgget(IPC_PRIVATE, IPC_CREAT | IPC_EXCL | 0644);

0644表示用户读写,组读,其他读。

为使得客户进程和服务器进程能使用同一个IPC对象进行通信,它们需要知道IPC对象对应的键值。可以把键值定义在一个公共头文件中,客户进程和服务器进程都带上这个头文件,这样它们就都知道了键值,之后由服务器进程用该key值创建IPC对象,二者就可以借此IPC对象进行通信了。可以通过ftok函数生成键值,该函数通过指定的路径名和项目id生成一个键值。

#include <sys/ipc.h>
key_t ftok(const char *path, int id);
//成功时返回键,失败返回(key_t)-1

3.1 消息队列

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。

每个消息队列都关联一个msqid_ds结构。

struct msqid_ds{
    struct ipc_perm msg_perm;    //权限结构
    msgqnum_t	    msg_qnum;	 //队列中的消息数
    msglen_t 		msg_qbytes;  //队列的最大bytes数
    pid_t           msg_lspid;   //最后执行msgsnd的进程pid
    pid_t           msg_lrpid;   //最后执行msgrcv的进程pid
    time_t          msg_stime;   //最后msgsnd时间
    time_t          msg_rtime;   //最后msgrcv时间
    time_t          msg_ctime;   //最后改变的时间
    ...
};

使用msgget函数创建新的或打开已有的消息队列。

#include<sys/msg.h>
int msgget(key_t key, int flag);
//成功时返回消息队列ID,失败返回-1
  • key:键
  • flag:标识

参数key和flag设置方法在上文已提及,这里包括后文就不再赘述。
创建新的队列时会初始化一个mspid_ds结构体与队列关联。

使用msgctl对队列进行多种操作。

#include<sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//成功时0,失败返回-1
  • msqid表示队列id
  • cmd指定对msqid指定的队列要进行的命令,命令有:
    – IPC_STAT取此队列的msqid_ds结构,并将其存放在buf指向的结构中;
    – IPC_SET将字段msg_perm.uid、msg_prem.gid、msg_prem.mode和msg_qbytes从buf所指向的结 - 构复制到这个队列相关的msqid_ds结构中。此命令只能由有效用户ID等于msg_perm.cuid或msg_perm.uid的进程或具有超级用户特权的进程执行。
    –IPC_RMID从系统中删除该消息队列以及其中的数据,立即生效,仍在使用该消息队列的进程在试图对消息队列操作时将发生EIDRM错误。此命令的执行权限也与上一命令相同。
  • buf指向一个msqid_ds结构
    使用msgsnd函数将数据放到消息队列中,即新消息添加到队尾。
#include<sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
//成功时0,失败返回-1
  • msqid表示队列id
  • ptr是一个指向消息的指针,消息的定义格式如下
struct mymesg
{
	long mtype; // (正的)长整型消息类型
	//长度为nbytes的消息正文,例如书上的:
	//char mtext[512]; 
}
  • nbytes表示消息正文大小
  • flag为可选项,设置

使用msgrcv从队列中取用消息。

#include<sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
//成功时返回消息部分数据的长度,失败返回-1
  • msqid表示队列id
  • ptr含义同msgsnd,指向一个消息结构(包含长整型数和消息数据)
  • nbytes指定数据缓冲区的长度
  • type指定想要哪一种消息
    – 设置为0返回队列中的第一个消息,
    – 设置为>0返回队列中消息类型为type的第一个消息,
    – 设置为<0返回队列中消息类型值小于等于type绝对值的消息,若有多个这样的消息则返回类型值最小的消息。
  • flag设置MSG_NOERROR位,消息长度超过nbytes会被截断;指定IPC_NOWAIT操作不阻塞,无消息可用时返回-1

3.2 信号量

不同于前几种IPC,信号量是一个计数器,用于为多个进程提供共享数据对象的访问。
信号量的基本原理为:在获取共享资源时要对信号量进行判断,若信号量为正则可以使用,使用后信号量减一,若信号量为0则不能使用,进程休眠,直到信号量大于0。当进程不再使用由一个信号量控制的共享资源时,对应的信号量加一,若有进程休眠则唤醒。
XSI 信号量的使用要维护一个信号量集合,内部包含一个或多个信号量。内核为每个信号量集合维护一个semid_ds结构。

struct semid_ds{
	struct ipc_perm sem_perm;  //权限
	unsigned short  sem_nsems; //集合中信号量数量
	time_t          sem_otime; //最后semop()的时间
    time_t          sem_ctime; //最后改变的时间
    ...
}

每个信号量由一个无名结构表示,它至少包含下列成员

struct {
	unsigned short semval;//信号量值
	pid_t 		   sempid;//最后一次操作的进程id
	unsigned short semncnt;//等待semval变为大于当前值的进程数
	unsigned short semnznt;//等待semval变为0的进程数
}

使用semget函数获取信号量ID。

#include<sys/sem.h>
int semget(key_t key, int nsems, int flag);
//成功时返回信号量ID,失败返回-1

该get函数除了key和flag参数之外还多了个nsems参数。若是创建新信号量集合,需通过此参数指定集合中的信号量数量,并且创建信号量集合时会初始化关联的semid_ds结构;若是使用现有集合的信号量,则指定nsems为0.

使用semctl函数对信号量进行操作

 #include<sys/sem.h>
 int semctl(int semid, int semnum, int cmd, /*union semun arg*/);
 //返回值得看具体操作
  • semid:信号量ID
  • semnum:指定信号量集合中的成员,范围[0,nsems - 1]
  • cmd:命令,例如IPC_STAT、IPC_SET和IPC_RMID的效果与msgctl的对应操作相似,GETVAL命令返回成员semnum的semval值,。。。太多了先不写了。
  • arg:该参数为可选参数,该参数是一个联合体,结构如下
union semun {
	int              val;    //用于SETVAL命令
    struct semid_ds *buf;    //用于IPC_STAT、IPC_SET命令
    unsigned short  *array;  //用于GETALL、SETALL命令
};

通过semop函数进行信号量操作,不过与semctl所谓的操作不同,该函数将发挥信号量的同步功能。

 #include<sys/sem.h>
 int semop(int semid, struct sembuf []semoparray, size_t nops);
 //成功时返回0,失败返回-1
  • semid:信号量ID
  • semoparray:指向一个sembuf结构组成的信号量操作数组,函数执行操作数组指定的操作,sembuf结构为
struct sembuf {
	unsigned short sem_num;  //信号量成员编号,即操作的对象,为(0, 1, ..., nsems - 1)中的值
    short          sem_op;   //信号量操作,操作完成后该值加到信号量上
    short          sem_flg;  //信号量标识,可设置IPC_NOWAIT,SEM_UNDO
}
  • nops:上述数组的大小,即操作数量

调用该函数时,具体操作依赖sembuf结构中的sem_op参数的值进行执行:

  • 当sem_op > 0时,直接返回并把该值加到信号量值上,若设置了SEM_UNDO则撤销该操作
  • 当sem_op < 0时,
    — 若信号量值大于sem_op的绝对值,则令信号量的值减去sem_op的绝对值,若设置了SEM_UNDO则撤销该操作;
    — 若信号量值小于sem_op的绝对值,则
    —— 若设置了IPC_NOWAIT则semop出错,返回EAGAIN
    —— 若没设置IPC_NOWAIT则semncnt加1,并等待信号量大于sem_op的绝对值(semncnt减1,信号量值减去sem_op的绝对值,如果设置了SEM_UNDO则撤销该操作),或信号量被删除(出错返回EIDRM)或捕捉到一个信号(semncnt减1,出错返回EIDRM)
  • 当sem_op = 0时,进程希望信号量值变0
    — 若信号量当前值为0,则立即返回
    — 若信号量当前值非0,则
    —— 若设置了IPC_NOWAIT则semop出错,返回EAGAIN
    —— 若没设置IPC_NOWAIT则semnznt加1,并等待信号量值变0(semnznt减1),或信号量被删除(出错返回EIDRM)或捕捉到一个信号(semnznt减1,出错返回EIDRM)

3.3 共享存储

共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。

共享存储在使用时要注意同步问题,可用信号量等机制同步共享存储访问。
共享存储的一种形式是多个进程将同一个文件映射到他们的地址空间。与这种形式相比,XSI共享存储则没有相关的文件。

XSI共享存储段是内存的匿名段。

与前二者相似的,内核也为共享存储段维护一个结构:

struct shmid_ds {
    struct ipc_perm shm_perm;    //权限 
    size_t          shm_segsz;   //共享存储段大小(bytes)
    pid_t           shm_lpid;    //最后执行shmop()的进程ID 
    pid_t           shm_cpid;    //创建IPC内核对象的进程ID
    shmatt_t        shm_nattch;  //挂接进程个数
    time_t          shm_atime;   //最后挂接的时间
    time_t          shm_dtime;   //最后卸载的时间
    time_t          shm_ctime;   //最后改变的时间
    ...
};

使用shmget函数获取一个共享存储标识符(即ID)

#include <sys/shm.h>
int shmget(key_t key, size_t size, int flags);
//成功则返回共享存储段ID,失败返回-1

size参数指定共享存储段的长度(字节)。创建新共享存储段时需指定其大小,使用已创建的共享存储段则设为0。

使用shmctl函数对共享存储段进行操作

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//成功则返回0,失败返回-1
  • shmid:共享存储段ID
  • cmd:操作命令,可有:
    – IPC_STAT 取此段的msqid_ds结构,并将其存放在buf指向的结构中;
    – IPC_SET 将字段shm_perm.uid、shm_prem.gid、shm_prem.mode和从buf所指向的结构复制到这个段相关的shmid_ds结构中。此命令只能由有效用户ID等于shm_perm.cuid或shm_perm.uid的进程或具有超级用户特权的进程执行。
    –IPC_RMID 从系统中删除该共享存储段,注意只有连接计数减为0才实际删除该段。此命令的执行权限也与上一命令相同。
    –SHM_LOCK和SHM_UNLOCK,Linux和Sloaris提供的另外的命令。分别用于对共享存储段加锁和解锁。都只能由超级用户执行。
  • buf:指向一个shmid_ds结构,给操作命令使用

进程调用shmat将共享存储段连接到进程地址空间

#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
//成功则返回指向共享地址段的指针,并且关联的shmid_ds中的shm_nattch计数加1
//失败返回-1
  • shmid:共享存储段ID
  • addr:指定连接的地址,一般直接设为0表示由内核选择
  • flag:标识,指定SHM_RDONLY表示只读,否则以读写方式连接,指定SHM_RND,当addr非0时有用,一般都设成0就好了,所以不解释,(我也不懂啊)

进程调用shmdt使共享存储段与之分离

#include <sys/shm.h>
int shmdt(const void *addr);
//成功则返回0,并且关联的shmid_ds中的shm_nattch计数减1
//失败返回-1
  • addr:之前调用shmat的返回值,即指向共享存储段的指针

尾声:断断续续看了好几天才把这部分看完,而且也只是了解了相关原理,以及一些函数的作用,并对这些对象做了记录,记录也十分草率。具体如何实际使用这些IPC在之后的学习中再展开并记录吧,部分坑位已经留好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值