1、引言
2、管道
- 管道是
UNIX
系统IPC
(进程间通信)最古老的方式,所有UNIX
系统都提供这种通信机制 - 管道有以下局限性:
- 历史上,它们是半双工的(数据只能在一个方向上流动)。现在某些操作系统提供全双工管道,但是为了可移植性,应该按照半双工来进行编程
- 管道只能在具有公共祖先的两个进程间使用。通常一个管道由一个进程创建,在进程
fork
之后,父子进程之间通过该管道进行通信。
- 命名管道(FIFO)没有第二种限制,UNIX域套接字没有这两种限制
2.1、pipe函数
- 通过pipe函数创建管道
int pipe(int pipefd[2]);
- 函数成功返回后,
pipefd
中保存两个文件描述符:pipefd[0]
:读打开pipefd[1]
:写打开
即pipefd[1]
的输出是pipefd[0]
的输入。
- 对于全双工实现的管道,这两个文件描述符都是读/写打开的。
- 对于管道,通过
fstat
函数应用于其读端(pipefd[0]
)时,获得的stat
文件属性中的st_size
字段是无意义的。
- 函数成功返回后,
- 下图中左图显示管道的两端在一个进程中相互连接;右图则强调数据需要通过内核在管道中流动
2.2、管道常用操作
-
通常,进程先调用
pipe
,接着调用fork
,从而创建父进程与子进程间的IPC
通道。
-
对于从父进程到子进程的管道,父进程关闭管道的读端(
fd[0]
),子进程关闭写端(fd[1]
)。
-
对于从子进程到父进程的管道,父进程关闭管道的写端(
fd[1]
),子进程关闭读端(fd[0]
)。
-
-
当管道的一端被关闭后,遵守下列规则:
- 当
read
一个写端已被关闭的管道时,在所有数据都被读取后,read
返回0
,表示文件结束。如果管道的写端还在,就并不会产生文件的结束(如果管道没数据,则read阻塞) - 如果
write
一个读端已被关闭的管道,产生信号SIGPIPE
。该信号默认操作是终止进程,如果忽略该信号或捕获到该信号且捕获函数返回,则write
返回-1
,errno
置EPIPE
。
- 当
-
内核的管道缓冲区大小
- 在写\管道(或FIFO)时,常量
PIPE_BUF
规定了内核的管道缓冲区大小。- 如果对管道调用
write
且写入字节数小于PIPE_BUF
,那么此操作不会与其他进程对同一管道(或FIFO
)的write
操作交叉。 - 如果同时有多个进程写该管道(或
FIFO
)且我们write
的数据量大于PIPE_BUF
,那么我们所写的数据可能会与其他进程所写的数据相互交叉。
- 如果对管道调用
- 在写\管道(或FIFO)时,常量
-
实例1:父进程创建管道并
fork
子进程,指定父进程为管道写入端、子进程为读取端,父子进程间单向通信#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 */ /*父进程关闭管道的读端(fd[0])*/ close(fd[0]); write(fd[1], "hello world\n", 12); } else { /* child */ /*子进程关闭写端(fd[1])*/ close(fd[1]); n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } exit(0); }
-
实例2:试着编写一个程序,其功能是每次一页地显示已产生的输出。本例要求在命令行中有一个参数指定要显示的文件的名称。
#include "apue.h" #include <sys/wait.h> #define DEF_PAGER "/bin/more" /* default pager program */ int main(int argc, char *argv[]) { int n; int fd[2]; pid_t pid; char *pager, *argv0; char line[MAXLINE]; FILE *fp; if (argc != 2) err_quit("usage: a.out <pathname>"); if ((fp = fopen(argv[1], "r")) == NULL) err_sys("can't open %s", argv[1]); if (pipe(fd) < 0) err_sys("pipe error"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid > 0) { /* parent */ close(fd[0]); /* close read end */ /* parent copies argv[1] to pipe */ while (fgets(line, MAXLINE, fp) != NULL) { n = strlen(line); if (write(fd[1], line, n) != n) err_sys("write error to pipe"); } if (ferror(fp)) err_sys("fgets error"); close(fd[1]); /* close write end of pipe for reader */ if (waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); exit(0); } else { /* child */ close(fd[1]); /* close write end */ if (fd[0] != STDIN_FILENO) { /*fd[0]复制到标准输入*/ if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); close(fd[0]); /* don't need this after dup2 */ } /* get arguments for execl() */ if ((pager = getenv("PAGER")) == NULL) pager = DEF_PAGER; if ((argv0 = strrchr(pager, '/')) != NULL) argv0++; /* step past rightmost slash */ else argv0 = pager; /* no slash in pager */ if (execl(pager, argv0, (char *)0) < 0) err_sys("execl error for %s", pager); } exit(0); }
- 程序中通过管道将输出直接送到分页程序。程序中先创建一个管道,
fork
一个子进程,使子进程的标准输入成为管道的读端,然后调用exec
,执行用的分页程序。 - 在调用
fork
之前,先创建一个管道。调用fork
之后,父进程关闭其读端,子进程关闭其写端。然后子进程调用dup2
,使标准输入成为管道的读端。当执行分页程序时,其标准输入将是管道的读端。 - 将一个描述符复制到另一个上(在子进程中,
fd[0]
复制到标准输入),在复制之前应该比较该描述符的值是否已经具有所希望的值。如果shell
没有打开标准输入,那么程序开始处的fopen
应已使用描述符0
,所以fd[0]
决不会等于标准输入。 - 在尝试使用环境变量
PAGER
获得用户分页程序名称时,如果操作没有成功,那么将使用系统默认值。
- 程序中通过管道将输出直接送到分页程序。程序中先创建一个管道,
-
实例3:8.9节中
TELL_WAIT
、TELL_PARENT
、WAIT_PARENT
、TELL_CHILD
和WAIT_CHILD
的管道的实现。#include "apue.h" static int pfd1[2], pfd2[2]; void TELL_WAIT(void) { if (pipe(pfd1) < 0 || pipe(pfd2) < 0) err_sys("pipe error"); } void TELL_PARENT(pid_t pid) { if (write(pfd2[1], "c", 1) != 1) err_sys("write error"); } void WAIT_PARENT(void) { char c; if (read(pfd1[0], &c, 1) != 1) err_sys("read error"); if (c != 'p') err_quit("WAIT_PARENT: incorrect data"); } void TELL_CHILD(pid_t pid) { if (write(pfd1[1], "p", 1) != 1) err_sys("write error"); } void WAIT_CHILD(void) { char c; if (read(pfd2[0], &c, 1) != 1) err_sys("read error"); if (c != 'c') err_quit("WAIT_CHILD: incorrect data"); }
- 程序对应的图例如下
- 程序对应的图例如下
3、函数popen和pclose
-
popen
函数通过管道执行命令行程序。popen
函数创建一个管道,fork
一个子进程,关闭未使用的管道端,执行一个shell
命令,然后通过pclose
函数等待命令终止。FILE *popen(const char *command, const char *type); int pclose(FILE *stream);
popen
函数- 先执行
fork
,然后调用exec
执行command
,并返回一个标准I/O
文件指针。调用者通过该文件指针与命令程序通信。 - 如果
type
参数是"r"
,则文件指针连接到command
的标准输出 - 如果
type
参数是"w"
,则文件指针连接到commad
的标准输入。
- 注意,
command
按照以下方式执行
即当sh -c command
popen
执行命令时,相当于调用
因此execl("/bin/sh","sh","-c",command,(char*)0);
command
中可以使用shell
支持的任何特殊字符。如fp = popen("ls *.c","r"); fp = popen("cmd 2>&1","r");
- 注意,与
system
函数类似,popen
不能由设置用户ID
位或设置组ID
位的程序调用。因此如果一个进程正在以特殊的权限运行(由于设置用户ID
位或设置组ID
位造成),它又想生成另一个进程执行另一个程序,则它不能使用popen
,防止特殊权限传递下去。
- 先执行
pclose
函数:- 关闭标准
I/O
流,等待命令终止,然后返回shell
的终止状态(与system
函数返回值类似)。
- 关闭标准
-
实例1:用
popen
重写第2节中的实例2,其结果如下:#include "apue.h" #include <sys/wait.h> /*如果shell变量PAGER已经定义,且其值为非空, 则使用其值,否则使用字符串more*/ #define PAGER "${PAGER:-more}" /* environment variable, or default */ int main(int argc, char *argv[]) { char line[MAXLINE]; FILE *fpin, *fpout; if (argc != 2) err_quit("usage: a.out <pathname>"); if ((fpin = fopen(argv[1], "r")) == NULL) err_sys("can't open %s", argv[1]); if ((fpout = popen(PAGER, "w")) == NULL) err_sys("popen error"); /* copy argv[1] to pager */ while (fgets(line, MAXLINE, fpin) != NULL) { if (fputs(line, fpout) == EOF) err_sys("fputs error to pipe"); } if (ferror(fpin)) err_sys("fgets error"); if (pclose(fpout) == -1) err_sys("pclose error"); exit(0); }
-
实例2:函数
popen
和pclose
的实现,略过。 -
实例3:考虑一个应用程序,它向标准输出写一个提示,然后从标准输入读
1
行。使用popen
,可以在应用程序和输入之间插一个程序一遍对输入进行变化处理。下图展示了这种情况下的进程安排。
首先展示过滤程序#include "apue.h" #include <ctype.h> int main(void) { int c; while ((c = getchar()) != EOF) { if (isupper(c)) c = tolower(c); if (putchar(c) == EOF) err_sys("output error"); if (c == '\n') fflush(stdout); } exit(0); }
将上述程序编译成可执行文件
myuclc
,然后下面的程序会用popen
调用它。#include "apue.h" #include <sys/wait.h> int main(void) { char line[MAXLINE]; FILE *fpin; if ((fpin = popen("myuclc", "r")) == NULL) err_sys("popen error"); for ( ; ; ) { fputs("prompt> ", stdout); fflush(stdout); if (fgets(line, MAXLINE, fpin) == NULL) /* read from pipe */ break; if (fputs(line, stdout) == EOF) err_sys("fputs error to pipe"); } if (pclose(fpin) == -1) err_sys("pclose error"); putchar('\n'); exit(0); }
标准输出通常是行缓冲的,而提示并不包含换行符,所以在写了提示之后,需要调用
fflush
4、协同进程
-
当一个程序既产生某个过滤程序的输入,又读取该过滤程序的输出,该过滤程序就变成了协同进程。
- 协同进程通常在shell后台运行,其标准输入和标准输出通过管道连接到另一个程序。
popen
只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个连到其标准输入,另一个则来自其标准输出。我们想将数据写到其标准输入,经其处理后,再从其标准输出读取数据。
-
实例:用一个实例来观察协同进程。进程创建了两个管道:一个是协同进程的标准输入,另一个是协同进程的标准输出。下图展示了这种安排。
下面程序是一个简单的协同进程,他从其标准输入读取两个数,计算它们的和,然后将和写至其标准输出#include "apue.h" int main(void) { int n, int1, int2; char line[MAXLINE]; while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { line[n] = 0; /* null terminate */ if (sscanf(line, "%d%d", &int1, &int2) == 2) { sprintf(line, "%d\n", int1 + int2); n = strlen(line); if (write(STDOUT_FILENO, line, n) != n) err_sys("write error"); } else { if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) err_sys("write error"); } } exit(0); }
对程序进程编译,将其可执行目标代码存入名为
add2
的文件中。#include "apue.h" static void sig_pipe(int); /* our signal handler */ int main(void) { int n, fd1[2], fd2[2]; pid_t pid; char line[MAXLINE]; if (signal(SIGPIPE, sig_pipe) == SIG_ERR) err_sys("signal error"); if (pipe(fd1) < 0 || pipe(fd2) < 0) err_sys("pipe error"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid > 0) { /* parent */ close(fd1[0]); close(fd2[1]); while (fgets(line, MAXLINE, stdin) != NULL) { n = strlen(line); if (write(fd1[1], line, n) != n) err_sys("write error to pipe"); if ((n = read(fd2[0], line, MAXLINE)) < 0) err_sys("read error from pipe"); if (n == 0) { err_msg("child closed pipe"); break; } line[n] = 0; /* null terminate */ if (fputs(line, stdout) == EOF) err_sys("fputs error"); } if (ferror(stdin)) err_sys("fgets error on stdin"); exit(0); } else { /* child */ close(fd1[1]); close(fd2[0]); if (fd1[0] != STDIN_FILENO) { if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); close(fd1[0]); } if (fd2[1] != STDOUT_FILENO) { if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); close(fd2[1]); } if (execl("./add2", "add2", (char *)0) < 0) err_sys("execl error"); } exit(0); } static void sig_pipe(int signo) { printf("SIGPIPE caught\n"); exit(1); }
- 该程序从标准输入读取两个数之后调用
add2
协同进程,并将协同进程送来的值写到其标准输出。 - 这个程序创建了两个管道,父进程、子进程各自关闭它们不需要使用的管道端。必须使用两个管道:一个作为协同进程的标准输入,另一个则用作它的标准输出。然后,子进程通过调用
dup2
使管道描述符移至其标准输入和标准输出,最后调用了execl
。 - 如果程序在等待输入的时候杀死了
add2
协同进程,然后又输入两个数,那么程序对没有读进程的管道进行写操作时,会调用信号处理程序。
- 该程序从标准输入读取两个数之后调用
-
实例:如果将协同进程
add2
的实现换成下面程序的标准I/O
,而不是底层I/O
如read
和write
,则会发生什么?#include "apue.h" int main(void) { int int1, int2; char line[MAXLINE]; while (fgets(line, MAXLINE, stdin) != NULL) { if (sscanf(line, "%d%d", &int1, &int2) == 2) { if (printf("%d\n", int1 + int2) == EOF) err_sys("printf error"); } else { if (printf("invalid args\n") == EOF) err_sys("printf error"); } } exit(0); }
- 题目中的答案是:不能运行。因为标准I/O库函数是有缓冲区的,对于管道类型,标准I/O库默认使用全缓冲。当add2从其标准输入读取而发生阻塞时,程序从管道读也会发生阻塞,于是会产生死锁。此时就应该根据需要调用setvbuf函数更改缓冲区类型。
5、FIFO
FIFO
有时被称为命名管道,匿名管道只能在两个相关进程之间使用(这两个进程有一个共同的祖先进程)。但是FIFO
没有这种限制,不相关的进程也可以通过它交换数据。FIFO
是一种文件类型(七大文件类型之一),通过stat
结构的st_mode
成员可以知道该文件是否是FIFO
类型,通过S_ISFIFO
宏对st_mode
进行测试。
5.1、 mkfifo函数
- 通过mkfifo函数创建命名管道文件。
int mkfifo (const char *path, __mode_t mode); int mkfifoat (int fd, const char *path, __mode_t mode);
- mode参数与open函数中的mode参数相同。
- 当创建了一个
FIFO
文件或者文件系统中已有该FIFO
文件,就可以通过open
来打开它,然后使用正常的文件I/O
函数(如read
、write
、close
、unlink
等)与之交互。
5.2、FIFO注意事项
-
当以
O_NONBLOCK
标志open
一个FIFO
时:- 当没有用
O_NONBLOCK
标志时,只读open
要阻塞到某个其他进程写打开这个FIFO
为止;类似的,只写open
要阻塞到某个其他进程读打开这个FIFO
为止。 - 如果有
O_NONBLOCK
,只读open
立即返回;对于只写open
,如果没有进程读打开该FIFO
,则只写open
返回-1
,errno
置ENXIO
。
- 当没有用
-
与匿名管道类似,若
write
一个尚无读进程的FIFO
,则产生信号SIGPIPE
。若某个FIFO
的最后一个写进程关闭了该FIFO
,则将为该FIFO
的读进程产生一个文件结束标志。 -
一个给定的FIFO有多个写进程是很常见的。如果不希望多个进程所写的数据交叉,则必须要原子的写操作。和匿名管道一样,常量
PIPE_BUF
说明了可被原子写到FIFO
的最大数据量。
5.3、使用FIFO实例:客户进程-服务进程通信
-
FIFO的一个用途就是在客户进程和服务器进程之间传送数据。
-
如果有一个服务器进程,它与很多客户进程有关,每个客户进程都可将请求写到一个该服务器进程创建的众所周知的
FIFO
中
-
因为该FIFO有多个写进程,因此客服进程发送给服务进程的请求长度要小于PIPE_BUF字节。这样避免客户进程之间写的内容交叉。
-
按照上面的安排,如果服务器进程以只读方式打开众所周知的
FIFO
,则当客户进程从1变成0
,服务器进程read
该FIFO
会读到文件结束标志。为使服务器免于处理这种情况,可以用读写方式open
该FIFO
。 -
上面的模型存在的问题:服务器进程不知道如何将回答送回各个客户进程。一种解决方案:每个客户进程都在其请求中包含它的进程ID。然后服务器进程为每个客户进程创建一个
FIFO
,使用的文件名以客户进程pid
为基础。
6、XSI IPC
- 有三种
IPC
称为XSI IPC
:消息队列、信号量、共享存储(共享内存)。 - 将这三种
IPC
称为XSI IPC
的原因是,它们不像其他IPC
使用文件系统命名空间(文件描述符),而是使用自己的一套命名空间(可以理解为自己的一套接口)。
6.1、 XSI IPC的标识符(id)和键(key)
-
每个
XSI IPC
结构(消息队列、信号量、共享内存)都用一个非负整数标识符来引用,即id
。对于IPC
结构的操作都需要知道其id
。但是需要区别的是,这里的IPC
结构的id
不是文件描述符,而是一个很大的整数(文件描述符取最小的未用值)。 -
id
是IPC
对象的内部名,我们通过该id
调用函数从而完成对IPC
的操作(如msgsnd
等)。但是如果牵扯到多个进程要操作同一IPC
结构,那么让这些进程都知道该id
值是一件较为困难的事情。因此,我们需要将每个IPC
对象都与一个键(key
)相关联,即把这个键当做IPC
对象的外部名。 -
我们统一一个键的命名方案(下文中的
ftok
函数)获得键值(类型是key_t
),然后在每个进程中调用指定的get
函数(msgget
等)来通过内核将这个键变换成id
,从而使得对于同一IPC
对象,不同进程都能获得其id值。 -
让多个进程(如客户进程和服务器进程)能够使用同一IPC对象的方法:
- 服务器进程以
IPC_PRIVATE
作为键来调用get
函数从而创建一个新IPC
结构,并将返回的id
值保存在某个文件中以便客户进程取用。- 缺点:文件系统需要服务器进程将整型
id
值写到文件中,然后客户进程又读这个文件获得该id
值。
- 缺点:文件系统需要服务器进程将整型
- 可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定以此键创建一个新的IPC结构
- 缺点:该键可能已引用某个IPC结构,此时get函数错误返回。服务器进程必须删除已存在的IPC结构然后再重新创建。
- 客户进程和服务器进程约定好同一个路径名和项目
id
(项目id
是0-255
的值),然后函数ftok
将这两个值变换为一个键。从而客户进程和服务器进程获得了相同的键值,并通过该键值调用get
函数获取引用IPC
对象的id
值。key_t ftok(const char *pathname, int proj_id);
- 其中
pathname
必须引用一个现有文件。 ftok提供的服务仅仅是通过路径名和项目id产生一个键,不涉及对IPC对象的任何操作。
- 其中
- 服务器进程以
6.2、 IPC对象的权限结构
-
每个
XSI IPC
对象,都有一个ipc_perm
结构与之关联,该结构规定了在这个IPC
对象的权限和所有者,至少有以下字段struct ipc_perm { __uid_t uid; /* 所有者有效用户ID */ __gid_t gid; /* 所有者有效组ID */ __uid_t cuid; /* 创建者有效用户ID */ __gid_t cgid; /* 创建者有效组ID */ unsigned short int mode; /* 读写权限 */ ... };
-
在通过
get
函数创建IPC
结构时,对所有字段赋初值。然后可以通过msgctl
、semctl
、shmctl
改变其中的uid
、gid
、mode
字段,这类似于对文件调用chown
和chmod
函数。 -
mode
字段由下图的值组成。注意,对于任何IPC
结构不存在执行权限。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/7e013a36987a94b79c6870cbe11c74b2.png)
6.3、XSI IPC的优缺点
-
第一个缺点:
IPC
对象在系统范围内起作用,没有引用计数,即进程终止不会导致IPC
对象销毁。- 比如一个进程创建了一个消息队列,放入几条消息,然后进程终止,那么该消息队列和其中的内容并不会被删除。它们会一直留在系统中直到发生下列事:某个进程通过
msgrcv
读消息,通过msgctl
删除消息队列;正在自举的系统删除消息队列。 - 相比于匿名管道,最后一个引用该管道的进程终止时管道被完全删除;对于
FIFO
,最后一个引用该FIFO
的进程终止时,虽然FIFO
文件没有被删除,但是留在FIFO
中的数据已被删除了。
-
第二个缺点:
- 这些
IPC
结构在文件系统中没有名字。因此不能通过操作文件名、操作文件描述符的函数来操作XIS IPC
对象(例如不能使用chmod
函数修改它们的访问权限,不能使用ls命令查看该IPC
对象)。 - 因为
XSI IPC
不使用文件描述符,因此不适用于多路转接函数(select
、poll
、epoll
),使得很难一次使用一个以上的XSI IPC
(可能造成忙等循环,不能使用多路转接I/O
的优点)。
- 这些
-
XIS 消息队列优点:
- 可靠的,流控制的,面向记录的,可以用非先进先出的次序处理
- 由于在操作消息队列前需要以某种方式获取其
id
,因此认为不是无连接的。 - 因为所有这些
IPC
限制在一台主机上,所以是可靠的 - 流控制的意思是,如果系统资源(缓冲区)短缺,或者接收进程不能再接收更多消息,则发送进程就要阻塞。当流控制条件消失时,发送进程唤醒。
- 可靠的,流控制的,面向记录的,可以用非先进先出的次序处理
7、 消息队列
- 消息队列存储在内核中,由消息队列
id
标识。 - 每个消息队列都有一个
msqid_ds
结构与之关联,该结构体定义了消息队列的当前状态。struct msqid_ds { struct ipc_perm msg_perm; /* 这个IPC对象的权限和所有者等信息 */ msgqnum_t msg_qnum; /* 消息队列中当前消息个数 */ msglen_t msg_qbytes; /* 队列上允许的最大字节数 */ __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; /* 最近修改时间 */ ... };
7.1、msgget函数
- 打开一个现有消息队列或者创建一个新的消息队列,并返回该消息队列
id
。int msgget(key_t key, int msgflg);
- 参数
key
:由ftok
产生的键或IPC_PRIVATE
- 为了引用已存在的
IPC
结构,该键必须是IPC
创建时使用的键值。 IPC_PRIVATE
用于创建IPC
结构。#define IPC_PRIVATE ((__key_t) 0) /* Private key. */
- 为了引用已存在的
- 参数
msgflg
:该IPC
结构的访问权限和几个标志。使用如IPC_CREAT|0666
- 访问权限:见
3.2
节,如0666
代表所有人均可读写 - IPC_CREAT:创建新的消息队列。 当
key
是IPC_PRIVATE
或者某个未与IPC
对象关联的键,则msgflg
参数应该指定IPC_CREAT
。 当要引用一个已有IPC
结构时,msgflg
参数不能指定IPC_CREAT
。 - IPC_EXCL与IPC_CREAT一同使用:表示如果要创建的消息队列已经存在,则返回错误
EEXIST
(类似于以O_CREAT
和O_EXCL
调用open
函数)。
- 访问权限:见
- 参数
- 注意,如果要使用一个已经存在的
IPC
结构并且知道该IPC
结构的id
标识,则可以不调用get
函数(可以简单理解为此时msgget
的功能只是通过键获取到对应的消息队列id
,因此如果知道了该id
值就不用调用msgget
了)。 - 每一个消息队列对象都有一个关联的
msqid_ds
结构体。在通过msgget
创建新消息队列时,会初始化msqid_ds
结构体以下成员:msg_perm
字段根据3.2节初始化,其中mode
成员(读写权限)按照msgflg
参数中的相应权限位设置msg_qnum
、msg_lspid
、msg_lrpid
、msg_stime
、msg_rtime
设置为0
msg_ctime
设置为当前时间msg_qbytes
设置为系统限制值
7.2、msgctl函数
- 通过msgctl对消息队列进行操作,类似于ioctl函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数msqid:消息队列id
- 参数cmd:指定要对消息队列进行的操作
- IPC_STAT,获取此消息队列的
msqid_ds
结构,并保存在buf
中 - IPC_SET,设置消息队列的
msqid_ds
结构中的部分字段值:msg_perm.uid
、msg_perm.gid
、msg_perm.mode
、msg_qbytes
。注意该操作需要以下权限之一:进程的有效用户id
等于msg_perm.cuid
或msg_perm.uid
,或者进程有超级用户权限。 - IPC_RMID,从系统中删除该消息队列及其中的内容。该删除操作立即生效,删除之后其他操作该消息队列的进程得到
EIDRM
错误。权限要求同IPC_SET
。
以上三个cmd
也可用于信号量和共享存储
- IPC_STAT,获取此消息队列的
7.3、msgsnd函数
- 向消息队列中放入一条消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- msqid:消息队列
id
- msgp:发送给队列的消息。
msgp
可以是任何类型的结构体,但第一个字段必须为long
类型,即表明此发送消息的类型(msgrcv
根据此来以非先进先出的次序取消息)。第二个字段是消息的具体内容。msgp
定义的参照格式如下:struct msgbuf { long mtype; /* 消息类型,必须大于零 */ char mtext[123]; /* 消息内容 */ };
- msgsz:要发送信息的长度(字节数),可以用以下的公式计算:
msgsz = sizeof(struct msgbuf) - sizeof(long);
。 - msgflg:发送方式
- 0:当消息队列满时,
msgsnd
将会阻塞,直到消息能写进消息队列。阻塞期间若该消息队列被删除,则返回EIDRM
;若被信号中断则返回EINTR
- IPC_NOWAIT:类似于文件
I/O
的非阻塞标志,当消息队列已满的时候,msgsnd
函数不等待立即返回EAGAIN。 - IPC_NOERROR:若发送的消息大于
msgsz
字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
- 0:当消息队列满时,
- msqid:消息队列
7.4、msgrcv函数
- 从消息队列取用数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
- msqid:消息队列标识符
- msgp:存放消息的结构体,结构体类型要与
msgsnd
函数发送的类型相同 - msgsz:要接收消息的大小,不含消息类型
long
占用的字节数 - msgtyp:要接收的消息类型
0
:接收第一个消息>0
:接收类型等于msgtyp
的第一个消息(由msgbuf
中的第一个成员mtype
消息类型决定)<0
:接收类型等于或者小于msgtyp
绝对值的第一个消息。若这种消息有若干个,则取类型值最小的消息。
- msgflg:接收方式
0
: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待。阻塞期间若该消息队列被删除,则返回EIDRM
;若被信号中断则返回EINTR
IPC_NOWAIT
:不阻塞,如果没有满足条件的消息则立即返回,此时错误码为ENOMSG
IPC_EXCEPT
:与msgtype
配合使用返回队列中第一个类型不为msgtype
的消息IPC_NOERROR
:如果队列中满足条件的消息内容大于所请求的size
字节,则把该消息截断,截断部分将被丢弃
- 返回值:
- 成功返回读取到的消息数据长度,失败返回
-1
并置位errno
- 成功返回读取到的消息数据长度,失败返回
7.5、消息队列总结:
- 由
msgsnd
和msgrcv
函数可知,消息队列是双向的。 - 消息队列被设计之初,可用的其他形式
IPC
只有半双工管道,那时消息队列提供高速的进程间通信。但是现如今,比肩消息队列的IPC
有很多种,且速度与之相比不落于下风。考虑到XSI IPC
的缺点:没有引用计数、不能通过文件名或文件描述符引用IPC
对象,因此不建议使用消息队列。
8、XSI 信号量
信号量本质是一个计数器,为多个进程提供对共享数据对象的访问。
- 信号量并不是单个的非负值,而是一个或多个信号量值得集合- 信号量的创建(semget)和初始化(semctl)是分开的。即不能原子的创建并初始化一个信号量集合- 有的进程终止时并没有释放已经分配给它们的信号量,所以我们不得不为这种情况担心(undo功能用于处理这种情况)
内核为每个信号量集合维护一个semid_ds结构
struct semid_ds
{<!-- -->
struct ipc_perm sem_perm; /* 这个IPC对象的权限和所有者等信息 */
__time_t sem_otime; /* 最近调用semop()时间 */
__time_t sem_ctime; /* 最近修改时间 */
__syscall_ulong_t sem_nsems; /* 集合中的信号量个数 */
...
};
集合中的每个信号量由以下无名结构表示,至少包含以下成员
struct {<!-- -->
unsigned short semval; /* 信号量值,总是>=0 */
pid_t sempid; /* 最后一次操作的进程pid */
unsigned short semcnt; /* 等待获取信号量资源的进程个数 */
unsigned short semzcnt; /* 等待semval==0的进程个数 */
};
5.1 semget函数
创建一个新信号量集合,或引用一个现有集合,并返回信号量集合id
int semget(key_t key, int nsems, int semflg);
参数的具体取值参考msgget函数。其中nsems参数为集合中信号量个数(创建新集合时可用)
当创建一个新信号量集合时,对semid_ds结构的下列成员赋初值
- sem_perm字段根据3.2节初始化,其中mode成员(读写权限)按照semflg参数中的相应权限位设置- sem_otime设置为0- sem_ctime设置为当前时间- sem_nsems设置为nsems
5.2 semctl函数
通过该函数对信号量集合进行各种操作(如初始化)
int semctl(int semid, int semnum, int cmd, .../* union semun arg */);
第四个参数可选,当使用某些cmd取值时,需要第四个参数。该参数类型是联合体semun,即为多个命令特定参数的联合
union semun {<!-- -->
int val; /* SETVAL用的值 */
struct semid_ds *buf; /* IPC_STAT, IPC_SET用的缓冲区 */
unsigned short *array; /* GETALL, SETALL用的数组 */
};
参数cmd可以操作于整个集合,或者集合中的某个指定信号量(根据第二个参数semnum指定,取值为0~nsems-1)
- IPC_STAT:取集合的semid_ds结构并保存在arg.buf中- IPC_SET:按照arg.buf值设置信号量集合semid_ds结构体的sem_perm.uid、sem_perm.gid和sem_perm.mode字段。权限要求同msgctl函数- IPC_RMID:从系统删除该信号量集合,删除立即发生。若删除后其他进程对该信号量集合进行操作,返回EIDRM。权限要求同上。- GETVAL:返回semnum信号量的semval值- SETVAL:设置semnum信号量的semval值为arg.val- GETPID:返回semnum信号量的sempid值- GETNCNT:返回semnum信号量的semncnt值- GETZCNT:返回semnum信号量的semzcnt值- GETALL:获得集合中所有信号量值,保存在arg.array数组中- SETALL:设置集合中所有信号量值为arg.array
5.3 semop函数
执行信号量集合的具体操作(PV操作)
int semop(int semid, struct sembuf *sops, size_t nsops);
参数sops是一个包含有nsops个元素的结构体数组,其中每个元素都是sembuf结构体,用于表示对该信号量的具体操作
struct sembuf
{<!-- -->
unsigned short int sem_num; /* 信号量下标(0~nsems-1) */
short int sem_op; /* 信号量操作 */
short int sem_flg; /* 操作方式:IPC_NOWAIT、SEM_UNDO */
};
其中sem_num表示信号量集合中的哪一个信号量。
sem_op表示对该信号量的具体操作(负值、0、正值)
- >0:对该信号量进行V操作。表示该进程要释放的以前占有的资源数。sem_op会加到semval上。如果指定了undo标志(sem_flg设置了SEM_UNDO),则从该进程的此信号调整值中减去sem_op。
- <0:P操作,表示该进程要获取的该信号量资源数。若semval大于等于sem_op绝对值(即当前信号量资源数够用),则从semval中减去sem_op绝对值。如果指定了undo,则从该进程的此信号调整值中加上sem_op绝对值。 如果semval小于sem_op绝对值(即当前信号量资源不够用),则
- - 若sem_flg有IPC_NOWAIT标志,则semop出错返回EAGAIN
- 若未指定IPC_NOWAIT,则该信号量的semncnt+1,进程挂起直到发生以下事件:
- - 此信号量semval变成大于等于sem_op绝对值,则信号量semncnt-1,从semval中减去sem_op绝对值。如果指定了undo,则从该进程的此信号调整值中加上sem_op绝对值。- 从系统中删除了该集合,出错返回EIDRM- 被信号中断,信号量semncnt-1,函数出错返回EINTR **==0:**表示调用进程希望等待到信号量semval为0
如果当前semval为0,则立即返回。否则
- 若sem_flg指定了IPC_NOWAIT,函数出错返回EAGAIN
- 若未指定IPC_NOWAIT,则信号量semzcnt+1,然后调用进程挂起直到以下事件发生:
- - 信号量semval变为0,则semzcnt-1,函数返回- 从系统中删除了该集合,出错返回EIDRM- 被信号中断,信号量semnznt-1,函数出错返回EINTR **注意,semop函数具有原子性。对于该函数中的各种操作,要么一个也不做,要么全部都执行。**
5.4 信号量调整值
前文中介绍了,有的进程终止时并没有释放已经分配给它们的信号量,所以我们不得不为这种情况担心(比如很多进程终止了但是没有释放它们占用的信号量资源,导致还没终止的进程无法获得这些被占用的资源)。undo功能(semop函数中sembuf.sem_flg字段设为SEM_UNDO)用于解决这种问题。
无论何时只要为信号量操作指定了SEM_UNDO,则内核会为该进程针对该信号量记录一个调整值。该调整值用于记录该进程对指定信号量占用的资源数。当进程终止时,就会根据该调整值将占用的资源进行释放(加到semval中)。
如果用SETVAL或SETALL调用semctl设置一个信号量的semval,则所有进程中的该信号量调整值都设为0(相当于该信号量初始化了)。
6. 共享存储(共享内存)
共享存储允许多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。
注意对于一个给定的共享存储区,多个进程之间要同步的访问。通常使用信号量(或者记录锁、互斥量)用于同步共享存储访问
XSI共享存储和内存映射的区别是,XSI 共享存储没有相关的文件,存储段是内存的匿名段。
内核为每一个共享存储段都维护一个shmid_ds结构体,至少包含以下字段
struct shmid_ds
{<!-- -->
struct ipc_perm shm_perm; /* 这个IPC对象的权限和所有者等信息 */
size_t shm_segsz; /* 共享存储段的大小(字节) */
__pid_t shm_cpid; /* 创建者进程pid */
__pid_t shm_lpid; /* 最近一次操作该IPC结构的进程pid */
shmatt_t shm_nattch; /* 当前连接数 */
__time_t shm_atime; /* 最近一次调用shmat()时间 */
__time_t shm_dtime; /* 最近一次调用shmdt()时间 */
__time_t shm_ctime; /* 最近一次通过shmctl()对IPC结构进行改变的时间 */
...
};
6.1 shmget函数
创建一个新共享存储段或引用一个现有共享存储段,返回该共享内存id
int shmget(key_t key, size_t size, int shmflg);
参数的具体取值参考msgget函数。
其中size参数为共享存储段大小(创建新共享存储段时可用),段内的内容初始化为0。通常将其向上取整为页长的整数倍。但是若size不是页长的整数倍,那么最后一页的余下部分是不可以使用的。
当创建一个新共享存储段时,对shmid_ds结构的下列成员赋初值
- shm_perm字段根据3.2节初始化,其中mode成员(读写权限)按照shmflg参数中的相应权限位设置- shm_lpid、shm_nattach、shm_atime、shm_dtime设置为0- shm_ctime设置为当前时间- shm_segsz设置为size
6.2 shmctl函数
对共享存储段进行多种操作
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数cmd:
- IPC_STAT:获取shmid_ds结构。将此共享存储段的shmid_ds结构复制到buf中- IPC_SET:按照buf设置共享存储段shmid_ds结构体的shm_perm.uid、shm_perm.gid和shm_perm.mode字段。权限要求同msgctl函数- IPC_RMID:在系统中删除该共享存储段。因为shmid_ds.shm_nattch维护了当前连接数,因此除非最后一个使用该段的进程终止或与该段分离,否则并不会实际的删除该共享存储段。但是该段id会被立即删除不能再被使用。权限要求同上。
6.3 shmat函数
通过shmat连接到一个已有的共享存储段,成功则返回指向共享存储段的指针,并使shmid_ds.shm_nattch+1
void *shmat(int shmid, const void *addr, int shmflg);
- 若addr为0,则共享存储段连接到由内核选择的第一个可用地址上(推荐)- 若addr非0,则此段连接到addr指定地址上。- shmflg:是一组标志位,通常为0。如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写的方式连接此段。
6.4 shmdt函数
当对共享存储段的操作结束,通过shmdt断开与共享存储段的连接,并使shmid_ds.shm_nattch-1。注意,这并不从系统中删除其标识符与相关的数据结构,仅仅是该进程断开与共享存储段的连接。
int shmdt(const void *shmaddr);
shmaddr是shmat函数的返回值
6.5 共享存储段的存储位置
共享存储段在堆和栈之间
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/4a902fbb77aa25e1b558870e66e032b9.png)
可以看出类似于内存映射(mmap),共享存储段也在堆和栈之间。区别是用mmap映射的存储段是与文件关联的,而共享存储段则没有这种关联。
6.6 通过内存映射完成共享存储的功能
可以通过对/dev/zero文件的内存映射,来达到类似于共享存储的功能。
由于/dev/zero文件提供无限的0,因此对该文件进行内存映射有以下功能:
- 以mmap第二个参数为长度(通常向上取整为页的倍数)创建一个存储区- 存储区内容都会被初始化为0- 如果设置了MAP_SHARED标志,则多个进程可以共享此存储区。
但是该技术只能在两个相关进程之间使用(即有相同的祖先进程),而不能是不相关进程之间使用。
7. POSIX 信号量
需要将POSIX 信号量和XSI 信号量区别开来。
POSIX信号量意在解决XSI 信号量的几个缺陷:
- POSIX 信号量接口效率更高- POSIX 信号量接口使用更简单,没有信号集。- POSIX 信号量在删除时表现更完美。直到该信号量的最后一次引用被释放时才真正删除该信号量(而XSI 信号量则是立即删除,其他使用该信号量的函数返回错误EIDRM)
7.1 匿名信号量和命名信号量
POSIX信号量分为匿名和命名两种。
其中匿名信号量只通过sem_t指针标识,即它是没有名字的。匿名信号量只存在于内存中,要求使用该信号量的进程必须能访问该内存。这意味着它只能在同一进程的线程之间使用,如果要在多个进程之间使用,要求将相同内存内容映射到它们的地址空间中(共享存储)。
而命名信号量则通过一个字符串名字标识,任何知道该名字的进程中的线程均可使用该命名信号量。
7.2 sem_open函数
通过sem_open函数创建一个新的命名信号量或者引用一个现有命名信号量,成功则返回指向该信号量的指针。该指针用于后续对该信号量的各种操作
sem_t *sem_open(const char *name, int oflag[,mode_t mode, unsigned int value]);
当引用现有信号量时,仅使用前两个参数。
name参数:信号量的名字,用于标识该信号量
**oflag参数:**创建信号量时,该参数为0。当该参数有O_CREAT,如果命名信号量不存在则创建一个新的;如果已存在则不会发生任何事。如果该参数指定O_CREAT|O_EXCL并且信号量已存在则出错返回。
**mode参数:**当创建新的命名信号量时可用,用于指定访问权限。其取值与打开文件的权限位相同,并且受文件创建屏蔽字的影响。
**value参数:**当创建信号量时,指定信号量初值
7.3 sem_close函数
当完成信号量操作时,可用该函数关闭指定信号量(关闭一个信号量并没有将他从系统中删除。POSIX 命名信号量是随内核持续的:即使当前没有进程打开该信号量,他的值仍保持。)
int sem_close(sem_t *sem);
如果进程没有调用sem_close而终止,则内核自动关闭任何打开的信号量。
注意,这不会影响信号量值,因为POSIX 信号量中没有undo机制。因此进程结束关闭信号量不会导致信号量值得改变。
7.4 sem_unlink函数
该函数用于从系统中删除命名信号量
int sem_unlink(const char *name);
如果该信号量没有被引用打开,则被直接销毁;如果该信号量正在被引用,则销毁将被延迟到最后一个打开的引用关闭。
7.5 POSIX 信号量的P操作
不像XSI 信号量的P操作那样可以减去指定值,POSIX 信号量的P操作一次只能-1。
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- sem_wait:调用该函数时若信号量值为0则阻塞。直到成功使信号量-1或者被信号中断返回。- sem_trywait:避免阻塞,若调用时信号量值为0则直接返回EAGAIN- sem_timedwait:指定阻塞时间(绝对时间,超时基于CLOCK_REALTIME即系统实时时间,从1970年开始的时间),如果超时还未成功则返回ETIMEDOUT。
7.6 POSIX 信号量的V操作
POSIX 信号量的V操作一次只能+1
int sem_post(sem_t *sem);
7.7 匿名信号量的操作
上面的各节都是对命名信号量的操作,用于多个进程间的进程同步使用。
如果想在单个进程中使用POSIX信号量,那么匿名信号量更加容易。
通过sem_init函数创建(初始化)一个匿名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数
sem
:指向信号量变量的指针(需要提前定义并将其地址传入)-pshared
:是否在多个进程中使用信号量(0线程同步;1进程同步)。如果多个进程间使用该匿名信号量,则需要sem参数指向两个进程的共享存储段范围内。-value
:信号量初始值
对匿名信号量使用结束,通过sem_destroy函数销毁
int sem_destroy(sem_t *sem);
调用该函数后,这个匿名信号量不能再使用,除非通过sem_init重新初始化它
通过sem_getvalue获取信号量当前值(命名、未命名信号量均可使用)
int sem_getvalue(sem_t *sem, int *sval);
对于匿名信号量的PV操作,与命名信号量接口一致
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
8. 进程通信总结
- 要会使用匿名管道和FIFO,因为这两种技术仍然可以有效的应用于大量应用程序- 在新应用程序中避免使用消息队列和信号量,而应该考虑全双工管道和记录锁,它们使用起来简单的多- 共享存储有自己的用处,但是可以通过mmap函数提供类似功能(见6.6节)