阅读前注意:APUE作者推荐本章了解相关技术即可,不要使用。
Chapter 15
15.2
管道是比较传统的IPC方式。有着两个缺点。
1、不是所有的系统都支持双通道管道。
2、管道只有用于由一个进程fork成的两个进程来使用。
FIFO可以解决第二个问题,UNIX domain sockets和命名流管道可以来解决两个问题。
一个管道可以用pipe来建立。
int pipe(int filedes[2]);
filedes[0]用来读取,filedes[1]用来写入。对filedes[0]读取的数据都源于filedes[1]的写入。
fstat函数可以测试管道,用S_ISFIFO这个参量。
先使用pipe然后使用fork之后生成的half-duplex管道:
parent: fd[0] <--rw-- child: fd[1]
parent: fd[1] --wr--> child: fd[0]
为实现单向数据传送,可以在parent,child端关闭不必要的file descriptor
如果对一个写端关闭的管道读,则read会返回0。
如果对一个读端关闭的管道写,则会产生sigpipe,同时write返回1,设置errno=EPIPE
如果写pipe或者fifo,PIPE_BUF限制了管道空间的大小。如果多个进程写入一个管道,且数据大小大于PIPE_BUF,则可能产生数据之间的交错。
15.3
FILE* popen(const char* cmdstring, const char* type);
int pclose(FILE*);
popen做一次fork再执行cmdstring,返回一个文件指针。如果type是'r',popen将cmdstring的输出接入。如果是'w'则将输入接入。pclose关闭输入输出流并且等待命令cmdstring结束,返回shell的状态。、
15.4
协作进程创建两个管道,其一接受输出,其一给定输入。
15.5
FIFO也叫做命名管道。普通管道只能用于同一个进程fork生成的进程之间,而FIFO可以使得不相关的进程交换数据。
FIFO也是一种文件,可以用S_ISFIFO来测试文件句柄是否是FIFO。
创建FIFO:
int mkfifo(const char* pathname, mode_t mode);
其中mode和open中的mode相同。
一旦创建了一个FIFO,则用open打开它。FIFO可以用文件的相应函数库来处理。
FIFO受到O_NONBLOCK标志影响。不设置此标志时,若无进程以写入权限打开FIFO则读被阻挡,若无进程以读取权限打开FIFO则写入被阻挡(等待相应的操作)。否则,对无写入的FIFO,可以打开读取,但是对无读取的FIFO则不能打开写入(errno=ENXIO)。
对于一个FIFO,通常有很多个写入进程并发。若不想要数据交错,则需要考虑原子操作。
FIFO的用处:1、SHELL的管道操作是通过FIFO进行的。2、FIFO用来作为客户/服务器传递数据的汇合点。
15.6 XSI IPC
XSI IPC包括消息队列,信号灯以及共享内存段。
所有的IPC结构在内核中都被映射成非负整数。访问都通过这个非负整数句柄来进行。协作的进程需要一个外部命名来使用,因此每个IPC对象都被赋予了一个Key。这个Key在IPC结构建立时给出。
对于CS结构,共享IPC的方法:
1、服务器可以使用IPC_PRIVATE KEY创建一个新的IPC结构,然后保存句柄到文件由客户端读取。这种手段也可以用来父子进程共享。
2、服务器可以和客户端共用一个编译前确定的KEY。
3、服务器和客户端可以共用一个路径名和项目编号,然后把它们变成一个key。(key_t ftok(const char* path, int id))
三个get函数(msgget,semget,shmget)都有两个相似的参数:key和一个整数flag。如果key是IPC_PRIVATE,或者key未和已经知道类型的IPC结构相连且有IPC_CREAT标志位,则会新建一个IPC结构。
不能使用IPC_PRIVATE引用一个已经存在的消息队列,因为这个kev值总是代表新建一个队列。如果要引用一个用IPC_PRIVATE key创建的队列,需要知道它的句柄。如果要创建新的IPC结构,需要确保不会引用一个有相同identifier的结构。可以使用IPC_CREAT和IPC_EXCL标志位,如果产生EEXIST的errno代表IPC已经存在。
每个XSI的IPC结构包含一个ipc_perm结构,这个结构定义了至少包含下面内容的权限和所有者信息:
struct ipc_perm { uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode; };
每个不同具体实现都可能包含其他信息。
IPC建立时已经初始化ipc_perm域。之后可以用msgctl,semctl或者shmctl来修改uid,gid,mode域。调用三个函数的程序必需是IPC结构的产生者或者超级用户。
缺点:
1、IPC结构是系统广度的,没有引用计数。它们会一直在系统中,除非调用msgrcv,msgctl例程,输入ipcrm命令,或者系统重启动。PIPE会在引用它的进程都结束时自动消失,而FIFO文件需要手工删除,但是它的数据在没有程序引用时消失。
2、这样的IPC结构在文件系统中无法引用。不能用IO函数(select/poll)。这样难以一次使用多个IPC结构,也不能用于文件或者设备IO。
15.7
消息队列是一个有着消息队列identifier的内核级消息链表。
创建消息链表,可以用msgget,新的消息通过msgsnd加入链表,每个消息都有一个正整数的类型域,一个非负的长度,和一个真实的长度。消息通过msgrcv接受。不需要通过FIFO形式接受消息,而可以通过他们的类型域接受。
每个链表都有msgid_ds结构相连。
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回message queue ID表示成功,-1失败。
int msgctl(int msgid, int cmd, struct msqid_ds* buf);
cmd包括:
IPC_STAT 获得msqid_ds结构
IPC_SET 拷贝msg_perm.uid,msg_perm.gid,msg_perm.mode和msg——qbytes数据到queue的相应位置。
IPC_RMID 删除消息队列。这是立即执行的。
int msgsnd(int msqid, const void* ptr, size_t nbytes, int flag);
ptr指示一个消息数据的结构,例如:
struct mymesg { long mtype; char mtext[NBYTES]; };
消息用msgrcv接受:
ssize_t msgrcv(int msqid, void* ptr, size_t nbytes, long type, int flag);
如果type==0,接收队列中最靠前的消息。type>0时寻找符合type的消息。type<0时寻找小于abs(type)的消息。
不推荐使用消息队列。
15.8 信号灯
对于多个进程共享的对象,使用信号灯来作为计数器。
使用共享资源,应该进行如下操作:
1、测试信号灯
2、如果信号灯的值为正,则使用资源,同时--信号灯的值
3、否则等待信号灯的值>0
当一个进程使用完了共享资源时,信号灯增加。其余等待信号灯的进程被叫醒。
使用信号灯时,信号灯操作必须为原子操作。最简单的信号灯为01信号灯。然而信号灯可以更复杂。XSI信号灯过于复杂:
1、信号灯的值必需被定义为一组值。
2、信号灯的建立和它的初始化是分离的。这是一个很大的漏洞。(非原子操作)
3、需要关注信号灯资源的浪费。
信号灯结构为semid_ds。取得信号灯结构句柄:
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回信号灯的ID。
信号灯的个数由nsems指定。nsems!=0时新建信号灯,否则引用以前的信号灯。
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
第四个参数是可选的,一个semun的union。跟随cmd的不同而改变内容。
函数semop进行对一个信号灯集合的组合操作
int semop(int semid, struct sembuf semoparray[], size_t nops);
15.9 共享内存
不同的进程共享内存。使用信号灯来管理共享的内存。
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
创建size_t大小的共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
...
void* shmat(int shmid,const void* addr, int flag);
将共享内存映射到进程的内存空间中。返回这个指针。
如果addr==0,内存段将被放在第一个内核认为可用的位置。
如果addr!=0,flag中没有SHM_RND,则被映射到addr处。
如果addr!=0,flag中有SHM_RND,则被映射到addr-(addr%SHMLBA)处。
为了移植性,应该使addr==0。
关闭进程的共享内存,则用
int shmdt(void* addr);
这个函数只是detach共享内存,并不消灭它。
Chapter 15
15.2
管道是比较传统的IPC方式。有着两个缺点。
1、不是所有的系统都支持双通道管道。
2、管道只有用于由一个进程fork成的两个进程来使用。
FIFO可以解决第二个问题,UNIX domain sockets和命名流管道可以来解决两个问题。
一个管道可以用pipe来建立。
int pipe(int filedes[2]);
filedes[0]用来读取,filedes[1]用来写入。对filedes[0]读取的数据都源于filedes[1]的写入。
fstat函数可以测试管道,用S_ISFIFO这个参量。
先使用pipe然后使用fork之后生成的half-duplex管道:
parent: fd[0] <--rw-- child: fd[1]
parent: fd[1] --wr--> child: fd[0]
为实现单向数据传送,可以在parent,child端关闭不必要的file descriptor
如果对一个写端关闭的管道读,则read会返回0。
如果对一个读端关闭的管道写,则会产生sigpipe,同时write返回1,设置errno=EPIPE
如果写pipe或者fifo,PIPE_BUF限制了管道空间的大小。如果多个进程写入一个管道,且数据大小大于PIPE_BUF,则可能产生数据之间的交错。
15.3
FILE* popen(const char* cmdstring, const char* type);
int pclose(FILE*);
popen做一次fork再执行cmdstring,返回一个文件指针。如果type是'r',popen将cmdstring的输出接入。如果是'w'则将输入接入。pclose关闭输入输出流并且等待命令cmdstring结束,返回shell的状态。、
15.4
协作进程创建两个管道,其一接受输出,其一给定输入。
15.5
FIFO也叫做命名管道。普通管道只能用于同一个进程fork生成的进程之间,而FIFO可以使得不相关的进程交换数据。
FIFO也是一种文件,可以用S_ISFIFO来测试文件句柄是否是FIFO。
创建FIFO:
int mkfifo(const char* pathname, mode_t mode);
其中mode和open中的mode相同。
一旦创建了一个FIFO,则用open打开它。FIFO可以用文件的相应函数库来处理。
FIFO受到O_NONBLOCK标志影响。不设置此标志时,若无进程以写入权限打开FIFO则读被阻挡,若无进程以读取权限打开FIFO则写入被阻挡(等待相应的操作)。否则,对无写入的FIFO,可以打开读取,但是对无读取的FIFO则不能打开写入(errno=ENXIO)。
对于一个FIFO,通常有很多个写入进程并发。若不想要数据交错,则需要考虑原子操作。
FIFO的用处:1、SHELL的管道操作是通过FIFO进行的。2、FIFO用来作为客户/服务器传递数据的汇合点。
15.6 XSI IPC
XSI IPC包括消息队列,信号灯以及共享内存段。
所有的IPC结构在内核中都被映射成非负整数。访问都通过这个非负整数句柄来进行。协作的进程需要一个外部命名来使用,因此每个IPC对象都被赋予了一个Key。这个Key在IPC结构建立时给出。
对于CS结构,共享IPC的方法:
1、服务器可以使用IPC_PRIVATE KEY创建一个新的IPC结构,然后保存句柄到文件由客户端读取。这种手段也可以用来父子进程共享。
2、服务器可以和客户端共用一个编译前确定的KEY。
3、服务器和客户端可以共用一个路径名和项目编号,然后把它们变成一个key。(key_t ftok(const char* path, int id))
三个get函数(msgget,semget,shmget)都有两个相似的参数:key和一个整数flag。如果key是IPC_PRIVATE,或者key未和已经知道类型的IPC结构相连且有IPC_CREAT标志位,则会新建一个IPC结构。
不能使用IPC_PRIVATE引用一个已经存在的消息队列,因为这个kev值总是代表新建一个队列。如果要引用一个用IPC_PRIVATE key创建的队列,需要知道它的句柄。如果要创建新的IPC结构,需要确保不会引用一个有相同identifier的结构。可以使用IPC_CREAT和IPC_EXCL标志位,如果产生EEXIST的errno代表IPC已经存在。
每个XSI的IPC结构包含一个ipc_perm结构,这个结构定义了至少包含下面内容的权限和所有者信息:
struct ipc_perm { uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode; };
每个不同具体实现都可能包含其他信息。
IPC建立时已经初始化ipc_perm域。之后可以用msgctl,semctl或者shmctl来修改uid,gid,mode域。调用三个函数的程序必需是IPC结构的产生者或者超级用户。
缺点:
1、IPC结构是系统广度的,没有引用计数。它们会一直在系统中,除非调用msgrcv,msgctl例程,输入ipcrm命令,或者系统重启动。PIPE会在引用它的进程都结束时自动消失,而FIFO文件需要手工删除,但是它的数据在没有程序引用时消失。
2、这样的IPC结构在文件系统中无法引用。不能用IO函数(select/poll)。这样难以一次使用多个IPC结构,也不能用于文件或者设备IO。
15.7
消息队列是一个有着消息队列identifier的内核级消息链表。
创建消息链表,可以用msgget,新的消息通过msgsnd加入链表,每个消息都有一个正整数的类型域,一个非负的长度,和一个真实的长度。消息通过msgrcv接受。不需要通过FIFO形式接受消息,而可以通过他们的类型域接受。
每个链表都有msgid_ds结构相连。
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回message queue ID表示成功,-1失败。
int msgctl(int msgid, int cmd, struct msqid_ds* buf);
cmd包括:
IPC_STAT 获得msqid_ds结构
IPC_SET 拷贝msg_perm.uid,msg_perm.gid,msg_perm.mode和msg——qbytes数据到queue的相应位置。
IPC_RMID 删除消息队列。这是立即执行的。
int msgsnd(int msqid, const void* ptr, size_t nbytes, int flag);
ptr指示一个消息数据的结构,例如:
struct mymesg { long mtype; char mtext[NBYTES]; };
消息用msgrcv接受:
ssize_t msgrcv(int msqid, void* ptr, size_t nbytes, long type, int flag);
如果type==0,接收队列中最靠前的消息。type>0时寻找符合type的消息。type<0时寻找小于abs(type)的消息。
不推荐使用消息队列。
15.8 信号灯
对于多个进程共享的对象,使用信号灯来作为计数器。
使用共享资源,应该进行如下操作:
1、测试信号灯
2、如果信号灯的值为正,则使用资源,同时--信号灯的值
3、否则等待信号灯的值>0
当一个进程使用完了共享资源时,信号灯增加。其余等待信号灯的进程被叫醒。
使用信号灯时,信号灯操作必须为原子操作。最简单的信号灯为01信号灯。然而信号灯可以更复杂。XSI信号灯过于复杂:
1、信号灯的值必需被定义为一组值。
2、信号灯的建立和它的初始化是分离的。这是一个很大的漏洞。(非原子操作)
3、需要关注信号灯资源的浪费。
信号灯结构为semid_ds。取得信号灯结构句柄:
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回信号灯的ID。
信号灯的个数由nsems指定。nsems!=0时新建信号灯,否则引用以前的信号灯。
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
第四个参数是可选的,一个semun的union。跟随cmd的不同而改变内容。
函数semop进行对一个信号灯集合的组合操作
int semop(int semid, struct sembuf semoparray[], size_t nops);
15.9 共享内存
不同的进程共享内存。使用信号灯来管理共享的内存。
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
创建size_t大小的共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
...
void* shmat(int shmid,const void* addr, int flag);
将共享内存映射到进程的内存空间中。返回这个指针。
如果addr==0,内存段将被放在第一个内核认为可用的位置。
如果addr!=0,flag中没有SHM_RND,则被映射到addr处。
如果addr!=0,flag中有SHM_RND,则被映射到addr-(addr%SHMLBA)处。
为了移植性,应该使addr==0。
关闭进程的共享内存,则用
int shmdt(void* addr);
这个函数只是detach共享内存,并不消灭它。