Unix环境高级编程学习笔记(十) 进程间通信

匿名管道(pipes)

匿名管道是一种未命名的、单向管道,通常用来在一个父进程和一个子进程之间传输数据。匿名的管道只能实现本地机器上两个进程间的通信,而不能实现跨网络的通信。使用pipes函数来创建管道:

int pipe(int filedes[2]);

该函数通过参数返回两个文件描述符,filedes[0] 用于读,filedes[1] 用于写,事实上,从 filedes[0] 中读出的数据即是向 filedes[1] 中写入的数据。pipes (除了基于流的管道)只能被用在拥有共同祖先的关联进程之间,且该祖先进程是管道的创建者。下面来看一个简单的例子,该例子来自《环高》:

#include "apue.h"
int
main(void)
{
	int n;
	int fd[2];
	pid_t pid;
	char line[MAXLINE];

	if (pipe(fd) < 0)
		err_sys("pipe error");
	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid > 0) {/* parent */
		close(fd[0]);
		write(fd[1], "hello world\n", 12);
	} else {/* child */
		close(fd[1]);
		n = read(fd[0], line, MAXLINE);
		write(STDOUT_FILENO, line, n);
	}
	exit(0);
}

PIPE_BUF 是管道的缓存区的大小,如果在管道的写端存在多个进程同时执行写操作,且存在进程写的字节数超过 PIPE_BUF ,那么,这些进程写入的数据就可能会发生交错,否则,则不会发生交错。

POSIX.1 允许实现支持全双工管道,即管道的两端都可用于读操作与写操作。

许多时候,我们需要开启一个子进程,然后令该子进程的标准输入或是标准输出关联上其父进程的某个文件描述符,以下函数为我们封装了这些操作:

FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);

popen 函数会使用 fork 和 exec 系函数去执行 cmdstring ,然后返回一个文件指针。如果 type 是 “r” ,则该指针将与子进程的标准输出关联;如果 type 是 "w" ,则该指针将与子进程的标准输入关联。

实际上,cmdstring 是通过 Bourne shell 来执行的,就好像键入命令:sh -c cmdstring。因此,有一点需要特别注意的是,最好不要在 set-user-ID 或是 set-group-ID 程序中使用 popen 函数,这可能会导致恶意用户获得高权限。

plose 函数关掉标准 I/O 流,并等待子进程结束,然后返回 shell 的终止状态。

FIFO文件

FIFO文件又称命名管道,是一种可用于无关进程间通信手段,FIFO文件的创建函数如下:

int mkfifo(const char *pathname, mode_t mode);

如果在正常模式下打开文件(即 O_NONBLOCK 没有被指定),则对于只读打开,它将阻塞直到有别的进程以写模式打开该文件,对于只写打开也同理如此。而如果在打开文件时指定了O_NONBLOCK 标志,则无论有没有进程以写模式打开该文件,以只读模式打开同样的文件都将直接返回;而如果不存在进程以只读模式打开该文件,则当以只写模式打开同样文件时将失败并返回 -1,同时,errno 将被设置为 ENXIO 。当 FIFO 的最后一个写进程关闭 FIFO 之后,读进程读 FIFO 将遇到文件结束。

XSI IPC

IPC 结构体是进程间的通信数据对象,主要有三种类型的 IPC 结构:消息队列,信号灯,以及共享内存。kernel 内的每一个 IPC 结构体都被一个非负整数标识。但是这个标识符只是 IPC 对象的内部名字,合作进程间还需要一种外部的命名机制使得它们可以使用相同的 IPC 对象来交互。出于此目的,每一个 IPC 对象都会和一个 KEY 关联,而这个 KEY 就是它的外部名称。

那么,该如何确定 KEY 值呢?有三种方式:

1. 合作进程对 KEY 值进行约定。这种方式的坏处是约定的 KEY 值可能己与某个 IPC 对象关联起来了。

2. 由其中一个进程在创建 IPC 对象时将 KEY 值指定为 IPC_PRIVATE ,这样可以保证使用的 KEY 值的唯一性,但该进程却不得不通过某种途径(例如放入文件中)将获得的标识符传递给其它进程。

3. 合作进程约定一个路径名和一个项目ID(1~255),然后使用 ftok 函数生成 KEY。函数声明如下:

key_t ftok(const char *path, int id);

path 参数必须引用一个存在的文件。需要注意的是,当使用相同的 ID 时,即使 path 不同,也有一定的概率创建出相同的 KEY。

每一个 IPC 对象都会关联一个 ipc_perm 结构体,该结构体定义了 IPC 对象的权限以及所有者,它至少包含以下的成员:

struct ipc_perm {
	uid_t uid; /*owner's effective user id */
	gid_t gid; /*owner's effective group id */
	uid_t cuid; /*creator's effective user id */
	gid_t cgid; /*creator's effective group id */
	mode_t mode; /*access modes */
	.
	.
	.
};

下面分开来讲讲这三种 IPC 结构。

消息队列

首先,是该结构体的创建,函数声明如下:

int msgget(key_t key, int flag);

它返回消息队列的标识符,如果 KEY 值没有和一个 已有的消息队列结构关联,则该函数将创建一个新的消息队列,否则,返回一个已有的消息队列的标识符。

如果一个进程创建了一个消息队列,然后进程终止,该消息队列是不会自动从系统中删除的。

每一个队列都有一个 msgid_ds 结构与其相关联,该结构至少包含以下成员:

struct msqid_ds {
	struct ipc_perm  	msg_perm;
	msgqnum_t 		msg_qnum; /*current number of messages in queue */
	msglen_t		msg_qbytes; /*maxmum number of bytes in queue */
	pid_t			msg_lspid; /*pid of last msgsnd() */
	pid_t			msg_lrpid; /*pid of last msgrcv() */
	time_t			msg_stime; /*last-msgsnd() time */
	time_t			msg_rtime; /*last-msgrcv() time */
	time_t			msg_ctime; /*last-change time */
	.
	.
	.
};

使用如下函数可以操纵该结构体:

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

cmd 参数指定了可以在消息队列上进行的操作:

IPC_STAT 获得指定队列的 msqid_ds 结构体,并将其存储在 buf 中.

IPC_SET 将 buf 中的如下成员赋值给与指定队列相关联的 msqid_ds 结构体的对应成员: msg_perm.uid, msg_perm.gid, msg_perm.mode, 以及 msg_qbytes. 当做,这个命令的执行是有权限需求的。

IPC_RMID 该命令可用于从系统中删除消息队列,任何还存在于队列中的数据都将被舍弃。 当做,这个命令的执行也是有权限需求的。

向消息队列中发送与接收 message 的函数如下:

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes , long type, int flag);

ptr 参数是 message 的首地址,而 nbtes 参数则说明了 message 的大小。每一个 message 都是由一个长整形的正整数(type)和消息数据组成,可用如下结构体表示:

struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };

可以使用 IPC_NOWAIT 宏来指定 flag 参数,它和文件 I/O 的 nonblocking I/O flag 的作用是一样的。

对于接收函数,如果 message 的大小大于 nbytes ,其形为要看 flag 的 MSG_NOERROR 位是否被设置。如果是,则 message 将被截断。否则,接收失败,函数返回 E2BIG,并且 message 仍然保留在队列里。type 参数可以用于指定我们想要接收的消息类型。

type == 0 返回队列中的第一个 message。

type > 0 返回队列中的第一个 type 等于指定 type 的 message。

type < 0 返回队列中的第一个 type 小于等于指定 type 的绝对值的 message。

信号灯(semaphore)

本来想把 semaphore 翻译信号量的,但前面已经把 signal 翻译成信号量了,参看网上的一些翻译,最终选了信号灯。这种机制主要用于进程间共享资源的同步。

还是从信号灯集的创建开始,其函数如下:

int semget(key_t key, int nsems, int flag);

key 和 flag 的作用与 msgget  的类似,这里就不再缀述,nsems 参数指定信号集中信号的个数,如果我们是通过该函数获得已有的信号集,则 nsems 可赋值为0。

kernel 为每一个信号集维护了一个 semid_ds 结构,其成员如下:

struct semid_ds {
	struct ipc_perm		sem_perm; 
	unsigned short		sem_nsems; /*number of semaphores in set */
	time_t			sem_otime; /*last-semop() time */
	time_t			sem_ctime; /*last-change time */
	.
	.
	.
};

可通过 semctl 函数对其进行修改:

int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

第四个参数是可选的,如果使用,需要传递如下所示的共同体:

           union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
           };

senmun 用于指定该信号集中的某一个成员, cmd 参数可以有10种情况,具体使用请参阅帮助文档。

信号集中的每一个信号灯都至少包含如下成员:

           unsigned short  semval;   /* semaphore value */
           unsigned short  semzcnt;  /* # waiting for zero */
           unsigned short  semncnt;  /* # waiting for increase */
           pid_t           sempid;   /* process that did last op */

要理解这些成员的含义,首先得弄清信号灯的应用规则。前面说了,信号灯机制是主要用于进程间共享资源的同步的,每一个信号灯就代表了一种资源,semval 实际上就是该种资源的当前可用数量。因此,某个进程如果要使用一定数量(比如 n)的该种资源,必须首先检查其对应信号灯的 semval 值,如果 semval >= n,则将 semval 减去 n,然后进程分配数量为 n 的该种资源。当归还资源时,应将 semval 加上 n。如果 semval < n,则进程被挂起,直到 semval >= n 为止。成员 semncnt 实际上就是被挂起的等待 semval 增加的进程数量。至于成员 semzcnt 是另一种挂起的进程,它们被唤醒的条件是 semval 变为0。

如上描述的那些操作都可由 semop 函数来实现:

int semop(int semid, struct sembuf semoparray[], size_t nops);

semoparray 指定了一个操作集和,它是一个数组,每一个操作都是数组中的一个元素,由 sembuf 结构体来表示,nops 则指定了数组的大小。sembuf 的成员如下:

struct sembuf {
           unsigned short sem_num;  /* semaphore number */
           short          sem_op;   /* semaphore operation */
           short          sem_flg;  /* operation flags */
};

sem_num 指定了集合中要操纵的信号灯,sem_op 则是要在该信号灯上执行的操作。如果 sem_op > 0,则代表归还资源,对应的 semval 会加上 sem_op,如果 sem_op < 0,则代表获取资源,规则和前面描述的一致。如果 sem_op == 0,则表示该进程想要等待 semval 变为0的条件。

sem_flag 可以指定的值有两个:IPC_NOWAIT 和 SEM_UNDO。如果 SEM_UNDO 被指定,则所进行的操作会看自动更新 undo count,当进程终止时,系统会根据 undo count 的值自动重做。另外,当使用 SETVAL 或是 SETALL 命令调用 semctl 函数时,undo count 值是会被自动置0的。而 IPC_NOWAIT 主要用于在获取资源时如果不够,进程不会等待,而是错误返回。

semop 是一个原子操作,如果 semoparray 中指定的操作有一个失败,则所有的操作都不会生效。

共享内存

创建共享内存的函数如下:

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

size 参数是这段内存的大小。

内核会为每一个共享内存维护一个结构体,其描述如下:

struct shmid_ds {
	struct ipc_perm		shm_perm; 
	size_t			shm_segsz; /*size of segment in bytes */
	pid_t			shm_lpid; /*pid of last shmop() */
	pid_t			shm_cpid; /*pid of creator */
	shmatt_t		shm_nattch; /*number of current attaches */
	time_t			shm_atime; /*last-attach time */
	time_t			shm_dtime; /*last-detach time */
	time_t			shm_ctime; /*last-change time */
	.
	.
	.
};

shmctl 是它的操作函数:

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

一个进程想要使用共享内存,首先得把它映射到自己的地址空间,使用 shmat 函数:

void *shmat(int shmid, const void *addr, int flag);

它返回映射的内存区的首地址。参数 addr 可以用来自己指定映射的地方,如果为 NULL,则交由系统指定。flag 可以指定 SHM_RND 宏,一旦指定,映射的地址将变成(addr – (addr 模 SHMLBA)) ,其中,SHMLBA 代表低边界地址倍数(low boundary address multiple)。

如果 flag 指定了 SHM_RDONLY 宏,它将以只读模式被映射,否则,则是读写模式。

使用 shmdt 函数解除映射关系:

int shmdt(void *addr);


你可能感兴趣的:(Unix环境高级编程学习笔记(十) 进程间通信)