匿名管道是一种未命名的、单向管道,通常用来在一个父进程和一个子进程之间传输数据。匿名的管道只能实现本地机器上两个进程间的通信,而不能实现跨网络的通信。使用pipes函数来创建管道:
int pipe(int filedes[2]);
#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);
实际上,cmdstring 是通过 Bourne shell 来执行的,就好像键入命令:sh -c cmdstring。因此,有一点需要特别注意的是,最好不要在 set-user-ID 或是 set-group-ID 程序中使用 popen 函数,这可能会导致恶意用户获得高权限。
plose 函数关掉标准 I/O 流,并等待子进程结束,然后返回 shell 的终止状态。
FIFO文件又称命名管道,是一种可用于无关进程间通信手段,FIFO文件的创建函数如下:
int mkfifo(const char *pathname, mode_t mode);
如果在正常模式下打开文件(即 O_NONBLOCK 没有被指定),则对于只读打开,它将阻塞直到有别的进程以写模式打开该文件,对于只写打开也同理如此。而如果在打开文件时指定了O_NONBLOCK 标志,则无论有没有进程以写模式打开该文件,以只读模式打开同样的文件都将直接返回;而如果不存在进程以只读模式打开该文件,则当以只写模式打开同样文件时将失败并返回 -1,同时,errno 将被设置为 ENXIO 。当 FIFO 的最后一个写进程关闭 FIFO 之后,读进程读 FIFO 将遇到文件结束。
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);
每一个 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 );
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 翻译信号量的,但前面已经把 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 */ };
信号集中的每一个信号灯都至少包含如下成员:
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 */
如上描述的那些操作都可由 semop 函数来实现:
int semop(int semid, struct sembuf semoparray[], size_t nops);
struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ };
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);
内核会为每一个共享内存维护一个结构体,其描述如下:
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);
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);