APUE第15章 进程间通讯

IPC类型

  1. 半双工管道、FIFO
  2. 全双工管道、命名全双工管道
  3. XSI消息队列、XSI信号量、XSI共享存储
  4. 消息队列(实时)、信号量、共享存储(实时)
  5. 套接字

管道

管道一般是半双工的,即便全双工也可以在半双工的条件下工作,一般为了移植性考虑不能假设他是全双工工作的。管道只能在具有公共祖先的进程中使用。所以一般是父进程创建管道后fork。而FIFO没有这种局限性。

1
2
3
#include <unistd.h>
int pipe(int fd[2]);
//通过参数返回两个文件描述符,fd[0]为读打开,f[1]为写打开

单个进程打开管道并没有什么意义,一般是fork后,父进程关闭读/写端,子进程关闭写/读端,当然也可以不关闭,只要注意是半双工的,同一时间消息只能有一个方向。

  1. 当读一个写端关闭的管道时,数据读完后再read会返回0,表示到文件尾,如果读端仍有进程则不会有这种情况。
  2. 如果写一个读端关闭则产生SIGPIPE信号,捕获或忽略后write返回-1,errno为EPIPE
  3. 写管道时,常量PIPE_BUF规定了内核缓冲区大小,如果写的时候字节数比这个小,则不会和其他写这个管道的write进行交叉操作,如果超过了,那么所写数据可能会与其他进程相互交叉(即不能保证原子性)。用
    1
    2
    3
    4
    5
    6
    7
    8
    4. 管道设置非阻塞模式的时候read和write失败都会立即返回-1

    ### popen和pclose
    popen的作用一般是先fork然后调用exec执行cmdstring,并返回一个标准I/O文件指针,如果type是"r",则文件指针连接到cmdstring的标准输出;如果type是"w",则文件指针指向cmdstring的标准输入。
    ```cpp
    #include <stdio.h>
    FILE *popen(const char *cmdstring, const char *type);
    int pclose(FILE *fp);//若成功返回cmdstring的终止状态,出错返回-1

pclose执行时,关闭标准I/O流,然后等待cmdstring的返回结果。

Notes:如果cmdstring不能执行,则pclose返回的终止状态与shell已执行exit(127)一样。

FIFO

命名管道。主要不同是让不相关的进程之间也可以交换数据。FIFO是一张文件类型。

1
2
3
4
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
//成功返回0,出错返回-1
  1. 如果path是绝对路径的时候,fd参数会被忽略
  2. 如果path是相对路径,则相对于fd(必须是目录)所在位置进行变化
  3. 如果path是一个特注释AT_FDCWD,则从当前路径开始
  4. 不论使用哪个函数,之后都需要通过open来打开

FIFO的设置和出错与管道基本类似,详情见《APUE》P446

XSI IPC

标识符和键

标识符是IPC对象的内部名,键(key)是IPC对象的外部名,一般创建XSI IPC的函数返回的都是key_t类型的键。

权限结构

XSI IPC为每一个IPC都关联了一个ipc_perm结构,规定了权限和所有者,至少包括以下成员(定义于

1
2
3
4
5
6
7
8
9
```cpp
struct{
uid_t uid; //owner's effective user id
gid_t gif; //owner's effective group id
uid_t cuid; //creator's effective user id
git_t cgid; //creator's effective group id
mode_t mode;//access mode
//......
};

优点和缺点

XSI IPC最基本的问题是没有引用计数,所以如果创建了消息队列,然后终止,那么内容不会被删除,而对于管道,如果引用计数为0就被删除了;对于命名管道,引用计数为0时内部内容销毁,仅保留命名管道的名字,直到被显式删除。

第二个问题是XSI IPC在文件系统中没有名字,无法使用文件处理的函数对他们的属性进行修改。这些形式的IPC不适用文件描述符,所以无法使用I/O多路复用。

优点是比较可靠,因为这些IPC形式被限制在一台主机上。

消息队列

1
2
3
4
5
6
7
8
9
10
11
struct msgid_ds{
struct ipc_perm msg_perm;
msgqnum_t msg_qnum;// num of messages on queue
msglen_t msg_qbtes;//max num of bytes on queue
pid_t msg_lspid;//pid of last msgsnd()
pit_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;//
//......
}

以上结构定义了队列的当前状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/msg.h>
int msgget(key_t key, int flag);
//用于创建消息队列,成功返回消息队列的ID,出错返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//成功返回0,出错返回-1
//cmd参数可以为IPC_STAT用于取数据,IPC_SET用于设置,IPC_RMID用于删除,只有root或者有效用户等于创建者可以成功删除
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
//成功返回0,出错返回-1
//flag可以设置IPC_NOWAIT进行不阻塞的操作
//自定义的*ptr结构如下
struct mymesg{
long mtype; //message type, must be positive
char mtxet[512];//content, of length nbytes
}
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
//成功返回消息数据那部分的长度,出错返回-1
//type可以指定想要那个消息,为0返回第一个,大于0返回队里消息类型为type的第一个消息,小于0返回绝对值小于type绝对值的消息,如果有若干个,先返回类型之最小的那个

信号量

本质是个计数器,对共享数据对象进行保护。要获得共享数据对象,应该先获得控制该资源的信号量,再获取,使用完,释放信号量使其加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;// num of semaphores in set
time_t sem_otime;// last-semop() time
time_t sem_ctime;// last-change time
//......
}
//信号量的无名结构
struct {
unsigned short semval; //semaphore value, always >=0
pid_t sempid; //pid for last operation
unsigned short semncnt;//num processes awaiting semval>curval
unsigned short semzcnt;//num processes awiting semval>=0
//......
};
//使用时,先用semget获取信号量ID,成功返回信号ID,否则-1
int semget(key_t key, int nsems, int flag);
//对信号量的操作
int semctl(int semid, int semnum, int cmd, .../* union semun arg* */);
//cmd的选项见《APUE》P457

共享存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 ofcurrent attaches
time_t shm_atime; //last-attach time
time_t shm_dtime; //last-detach time
time_t shm_ctime; //last-change time
//......
};
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
//返回值,成功返回对应的ID,出错返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *df);
//成功返回0,出错返回-1
//cmd参数见《APUE》P461
void *shmat(int shmid, const void *addr, int flag);
//成功返回指向共享段的指针,出错返回-1
int shmdt(const void *addr);
//删除成功返回0,否则返回-1

POSIX信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, .../* mode_t mode, unsigned int value */);
//成功返回指向信号量的指针,出错返回SEM_FAILED
int sem_close(sem_t *sem);
int sem_unlink(const char* name);
//均为删除信号量,成功返回0,出错-1
int sem_retwait(sem_t *sem);
int sem_wait(sem_t *sem);
//信号量-1,成功返回0,出错返回-1
#include <time.h>
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
//信号量-1,超时则失败
int sem_post(sem_t *sem);
//成功返回0,否则-1
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
//成功0,失败-1

一般而言sem_getvalue拿到后可能已经被修改了,所以一般只用于调试,使用这种信号量的原因是它比XSI IPC的信号量在Solaris系统中快了12%,在Linux上高了94%,因为Linux的实现将文件引射到了进程地址空间中,没有使用系统调用来操作各自的信号量。