进程间通信
# 进程间的通信
# 一、进程间通讯的概念
- 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
- 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )。
- 进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种 事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制 进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
# 二、Linux 进程间通信的方式
# 三、匿名管道
- 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式, 所有的 UNIX 系统都支持这种通信机制。
- 统计一个目录中文件的数目命令:
ls | wc –l
,为了执行该命令,shell 创建了两 个进程来分别执行 ls 和 wc。|
管道符,上面这个指令的意思就是 先执行 ls 获得一些数据再把数据交给wc
进行统计并输出
ls
进程的标准输出不是对应当前终端,而是对应管道的写入端,它输出不是输出到终端,而是输出到管道wc
进程的标准输入默认对应的是当前终端,但是这里对应的是管道的读取端,它会从管道中读取数据
# 四、管道的特点
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的 操作系统大小不一定相同。
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体, 但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据 的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺 序是完全一样的。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写 更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
- 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘 关系)之间使用。
- 左边的进程 fork 出一个子进程(右边),那它们会共享文件描述符表,假设我们父进程的文件描述符3对应文件A,4对应B,因为共享文件描述符表,所以子进程的文件描述符3也是对应A,4也是对应B
- 同理,如果父进程5对应管道的输入端 ,6对应输出端,那子进程的5也会对应管道的输入端,6对应输出端
- 这样,如果我们5进行写入,那么6就可以进行读取,(父子进程都可以对管道进行读写,只不过一次只能是一个方向,不可以又读又写)
- 这也是为什么我们匿名管道只能在具有公共祖先的进程之间使用的原因,因为他们有共享的文件描述符表。
注意: 我们这里虽然说是给管道写入了数据,但是我们是调用文件描述符来进行的,修改的是文件描述符所对应的文件/管道,所以不算写时复制
# 五、管道的数据结构
# 六、匿名管道的使用
- 创建匿名管道
- #include <unistd.h>
- int pipe(int pipefd[2]);
- 查看管道缓冲大小命令 ulimit –a
- 查看管道缓冲大小函数
- #include <unistd.h>
- long fpathconf(int fd, int name);
# 6.1 创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信
参数: int pipefd[2] 这个数组是一个传出参数
- pipefd[0] 对应的是管道的读端
- pipefd[1] 对应的是管道的写端
返回值:
成功返回 0 ,失败返回 -1
//管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
2
3
4
5
6
7
8
9
10
11
12
# 案例1
- 实现:子进程发送数据给父进程,父进程读取到数据输出
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 要在fork之前创建管道,不然子进程也会创建管道了
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
return -1;
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
printf("I am parent process,pid : %d\n", getpid());
// 父进程
char buf[1024] = {0};
while (1)
{
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
}
}
else if (pid == 0)
{
printf("I am child process,pid : %d\n", getpid());
// 验证管道是阻塞的,如果我们休眠了3秒,
// 那我们父进程也会等3秒后才输出,可以看出管道是阻塞的
sleep(3);
// 子进程,从管道的读取端读取数据
while (1)
{
char *str = "hello,I am child";
write(pipefd[1], str, strlen(str));
//隔一秒写一次,不然打印太快
sleep(1);
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- ⭕:可以看出管道默认是阻塞的,如果管道中没有数据,read阻塞,如果管道满了,write阻塞
# 案例2
- 父子进程都对管道进行写入和读取数据的操作
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 要在fork之前创建管道,不然就子进程也会创建管道了
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
return -1;
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
printf("I am parent process,pid : %d\n", getpid());
// 父进程
char buf[1024] = {0};
while (1)
{
// 读取管道中的数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
char *str = "hello,I am parent";
write(pipefd[1], str, strlen(str));
sleep(1);
}
}
else if (pid == 0)
{
printf("I am child process,pid : %d\n", getpid());
// 子进程
char buf[1024] = {0};
while (1)
{
// 向管道中写入数据
char *str = "hello,I am child";
write(pipefd[1], str, strlen(str));
sleep(1);
// 读取管道中的数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("child recv : %s, pid : %d\n", buf, getpid());
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
- 注意,父子进程他们对管道的操作顺序必须是不一致的,如果都先对管道进行读操作,那么管道都会阻塞(因为管道中没有数据可以读取,read操作阻塞),如果都先对管道进行写操作,那么写入的数据可能会错乱
🧡: 可以看到父子进程的进程号
💦:正常执行,交替输出,因为是子进程先去写入数据,父进程先读,所以父进程先输出他在管道里接收到的数据
❗:这里出现了个bug,就是父进程读到了自己写入到管道中的数据???
- 视频里是在父子进程进行写操作时把
sleep(1)
去掉了,才会出现进程自己写自己读的情况,但是我是加的也会,这里其实有点不理解为什么? - 不过解决这种问题,主要还是说管道尽量不要同一个进程又去读又去写,尽量一个进程读,那另一个进程就写,这样就不会出现这种问题,这样的作法我们可以用
close(pipefd[0])
和close(pipefd[1])
关闭父或子进程的管道写入端或输入端(完整代码在下方)
- 视频里是在父子进程进行写操作时把
🟩:多了个t,应该我们的输出是从管道读取数据到 buf 里再输出,父进程向管道写入的数据又比子进程的多一个字符,所以在父进程自己读取了自己写入的数据后,多出来的这个字符就被保存在父进程的 buf 里,所以出错后,后面每次父进程打印都多了一个
t
解决🟩这个问题,我们可以在每次读取管道数据到buf 里前,先把 buf清空
置字节字符串前n个字节为零且包括 "\0"。 bzero(buf, 1024); //把buf的前 1024位都置为 "\0"
1
2
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 要在fork之前创建管道,不然就子进程也会创建管道了
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
return -1;
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
printf("I am parent process,pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 父进程
char buf[1024] = {0};
while (1)
{
// 读取管道中的数据
bzero(buf, 1024);
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
// char *str = "hello,I am parent";
// write(pipefd[1], str, strlen(str));
// sleep(1);
}
}
else if (pid == 0)
{
printf("I am child process,pid : %d\n", getpid());
// 子进程
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while (1)
{
// 向管道中写入数据
char *str = "hello,I am child";
write(pipefd[1], str, strlen(str));
sleep(1);
// 读取管道中的数据
// bzero(buf, 1024);
// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 6.2 命令查看管道缓冲大小
ulimit -a
- 每个管道有8个块,每个块是 512byte,即4k
# 6.3 代码查看管道缓冲大小
#include <unistd.h>
long fpathconf(int fd, int name);
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
// 获取管道的大小
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size : %ld\n", size);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 可以看到,缓冲大小为 4K
# 6.4 匿名管道通信案例
/*
实现 ps aux | grep xxx 父子进程间通信
子进程: ps aux,子进程结束后,将数据发送给父进程
父进程: 获取到数据,过滤
pipe()
execlp()
子进程将标准输出重定向到管道的写端。 dup2()
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
int main()
{
// 创建一个管道(要在创建子进程之前曾经管道)
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
pid_t pid = fork();
if (pid > 0)
{
// 父进程
// 关闭读端
close(fd[1]);
// 从管道中读数据
char buf[1024] = {0};
int len = -1;
// sizeof(buf) -1 ,是因为有一个字符串结束符
while ((len = read(fd[0], buf, sizeof(buf) - 1)) > 0)
{
// 过滤数据输出
printf("%s", buf);
//清空 buf
memset(buf, 0, 1024);
}
//回收子进程资源,这里应该可以不写这句,写了也无所谓
//因为我们数据读完的话,那子进程也应该是写完了,(不然还没读完),所以不会产生僵尸进程,所以我认为可以不加wait()
wait(NULL);
}
else if (pid == 0)
{
// 子进程
// 关闭读端
close(fd[0]);
// 文件描述符的充重定向 stdout_fileno -> fd[1]
//标准输出本来是指向终端,这里被重定向到管道写入端,所以后面调用 ps aux进程,时数据就不是显示到终端,而是进入管道,等其他进程读取管道后再输出
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
execlp("ps", "ps", "aux", NULL);
//执行失败的话,用户区不会被替换,就会执行下面代码
perror("execlp");
exit(0);
}
else
{
perror("fork");
exit(0);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
- 这里我们没有像
ps aux | grep root
一样过滤,是直接输出了 ps aux | grep root
显示如下:- 只显示 root用户的进程
- 注意:因为管道缓冲的大小是一定的,所以子进程执行 ps aux 将数据写入管道,管道写满了,写端就会阻塞,然后读端读取数据,写端不阻塞继续写。。。这样把数据都读取出来
# 七、❗❗❗管道的读写特点
课程列表_牛客网 (nowcoder.com) (opens new window)
- 管道的读写特点:
- 使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
- 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
- 如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
- 如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
- 如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
- 使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
2
3
4
5
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
# 八、管道设置为非阻塞
/*
s设置管道非阻塞
int flags = fcntl(fd[0],F_GETFL) //获取原来的 falg
flags | O_NONBLOCK; //修改 falg的值
fcntl(fd[0],F_SETFL,flags); //设置新的falg
*/
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
// 要在fork之前创建管道,不然就子进程也会创建管道了
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
printf("I am parent process,pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 父进程
char buf[1024] = {0};
int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的 falg
flags |= O_NONBLOCK; // 修改 flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag
while (1)
{
// 读取管道中的数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d\n", len);
printf("parent recv : %s, pid : %d\n", buf, getpid());
//清空buf
memset(buf, 0, 1024);
sleep(1);
}
}
else if (pid == 0)
{
printf("I am child process,pid : %d\n", getpid());
// 子进程
// 关闭读端
close(pipefd[0]);
while (1)
{
// 向管道中写入数据
char *str = "hello,I am child";
write(pipefd[1], str, strlen(str));
sleep(5);
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
- 因为将读端设置为非阻塞了,读取不到数据时,返回-1,如果是阻塞状态且写端引用计数为0时,read返回0
- 我的理解:感觉正常阻塞情况下,如果子进程休眠,这时写管道没关闭,父进程read应该阻塞,但是在非阻塞情况下不会阻塞,它发现写端没有关闭,认为会有进程写数据,所以从头开始读,但是没读取到,所以会读取失败返回-1。如果子进程结束,那么写端关闭,父进程的读端知道不会有进程写数据,所以不会从头开始接着读,一直停留在文件末尾读取数据,所以一直返回0.
# 九、有名管道
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
- FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
- 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
- FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
# 十、有名管道的使用
1、通过命令创建有名管道
- mkfifo 名字
2、通过函数创建有名管道
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
1
2
3一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件 I/O 函数都可用于 fifo。如:close、read、write、unlink 等。
FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。
# 10.1 命令创建有名管道
mkfifo 有名管道名
- 刚创建的管道没有数据
- 🟥:被阻塞,没有将数据写入 fifo,fifo文件的数据都保存在内核中的内存,内存缓冲区,一旦我们程序结束了,缓冲区内的数据也没了
# 10.2 函数创建有名管道
/*
创建fifo文件
1. 通过命令: mkfifo 名字
2.通过函数:int mkfifo(const char *pathname, mode_t mode);
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname:管道名称的路径
- mode:文件的权限 和 open的mode是一样的
是一个八进制的数
返回值:成功返回 0,失败返回 -1
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit
int main()
{
int ret = mkfifo("fifio1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 10.3 有名管道写入和读出数据
//write.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit
#include <unistd.h> //access
#include <fcntl.h> //open
#include <string.h>
// 向管道中写数据
int main()
{
// 1.判断文件是否存在
int ret = access("test", F_OK);
if (ret == -1)
{
printf("管道不存在,创建管道\n");
// 2. 创建管道文件
ret = mkfifo("test", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 3.以只写的方式打开管道
int fd = open("test", O_WRONLY);
if (fd == -1)
{
perror("open");
exit(0);
}
// 写数据
for (int i = 0; i < 100; i++)
{
char buf[1024] = {0};
sprintf(buf, "hello,%d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//read.c
// 从管道中读数据
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit
#include <unistd.h> //access
#include <fcntl.h> //open
int main()
{
// 1.以只写方式打开管道
int fd = open("test", O_RDONLY);
if (fd == -1)
{
perror("open");
exit(0);
}
// 读数据
while (1)
{
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
//如果写进程被关闭,那么管道输入端计数为0,这时管道内如果没有数据,那么read返回0
if (len == 0)
{
printf("写端断开连接了...\n");
break;
}
printf("recv buf : %s\n", buf);
}
close(fd);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- 这里是在 终端1 执行
write可执行文件
,🟥因为这时没有管道文件,所以就创建,创建完后,会发现write是被阻塞的,因为此时我们还没执行read可执行文件
,去打开管道的读取端,所以被阻塞。 - 再到终端2 里执行
read可执行文件
,就会发现,程序正常执行,有数据被写入管道,也有数据被读出管道 - 我们在终端1 结束了write进程,会发现终端2的 进程也结束,是因为
write进程
结束,那么管道写入端计数为0,这时管道内如果没有数据,那么read返回0,而我们在read.c
做了判断,如果read返回0,就结束读数据,执行完程序。
- 这里和上面的区别就是这次是在终端2里 ,结束了 read进程,同样我们会发现终端1里的 write进程也会结束 ,原因是:
- read进程结束,管道的读取端计数为0,这时
write进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 导致进程异常终止。
- read进程结束,管道的读取端计数为0,这时
# 十一、❗❗❗有名管道注意事项
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
- 管道中有数据,read返回实际读到的字节数
- 管道中无数据:
- 管道写端被全部关闭,read返回0,(相当于读到文件末尾)
- 写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号),就好像一个水管流出端被封了起来,如果还要一直给它注水,管道会爆一样
管道读端没有全部关闭:
- 管道已经满了,write会阻塞
- 管道没有满,write将数据写入,并返回实际写入的字节数。
# 十二、有名管道实现聊天
# 12.1 图解
# 12.2 代码
//chatA.c
#include <stdio.h>
#include <unistd.h> //access
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 1.判断有名管道文件是否存在
int ret = access("fifo1", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 2.以只写的方式打开管道 fifo1
int fdw = open("fifo1", O_WRONLY);
if (fdw == -1)
{
perror("open");
exit(0);
}
printf("打开管道fifo1成功, 等待写入...\n");
// 3.以只读的方式打开管道 fifo2
int fdr = open("fifo2", O_RDONLY);
if (fdr == -1)
{
perror("open");
exit(0);
}
printf("打开管道fifo2成功, 等待读取...\n");
char buf[128];
// 4.循环的写读数据
while (1)
{
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 写数据
ret = write(fdw, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(0);
}
// 5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
// 调用失败 -1 ,管道写端计数为0,且管道内无数据就是0
if (ret <= 0)
{
perror("read");
exit(0);
}
printf("buf: %s\n", buf);
}
// 6.关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//chatB.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 1.判断有名管道文件是否存在
int ret = access("fifo1", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 2.以只读的方式打开管道fifo1
int fdr = open("fifo1", O_RDONLY);
if (fdr == -1)
{
perror("open");
exit(0);
}
printf("打开管道fifo1成功,等待读取...\n");
// 3.以只写的方式打开管道fifo2
int fdw = open("fifo2", O_WRONLY);
if (fdw == -1)
{
perror("open");
exit(0);
}
printf("打开管道fifo2成功,等待写入...\n");
char buf[128];
// 4.循环的读写数据
while (1)
{
// 5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if (ret <= 0)
{
perror("read");
break;
}
printf("buf: %s\n", buf);
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 写数据
ret = write(fdw, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(0);
}
}
// 6.关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 12.3 运行结果及解释1
这个要注意我们执行代码的顺序,我们
chatA 是先写再读
,chatB是先读再写
这里我们
./a
后会创建管道,但是不会输出 (打开。。。写入)和(打开。。。读取)这两句,因为管道一端被打开,另一端没有,这时进程会被阻塞,阻塞在open那一行的代码,直到我们再终端2 里 执行./b
两个进程才会输出(打开。。。这段文字)这里除了上面这种阻塞还有几种阻塞:
- 1、fgets()函数会阻塞,等待我们键入数据到终端(这里我一开始有个疑惑,就是为什么终端输入一次后还能继续输入,难道fgets是一直存在然后获取数据的?带着问题,就去查看了fgets函数的作用,看下面拓展)
- 2、管道两端都打开,但是管道内没有数据,那么read会阻塞
- 如我们在终端1输入1后终端2就打印除了2,等。。。
- 3、代码执行顺序导致阻塞
- 如5~8,我们写入完5后,6就输出了,但是我们没有在终端2去写入,而是又在终端1写入 7,这时终端2就没有了对应的显示,是因为我们代码里的每次循环都是先写后读或先读后写,如果我们读完一次后,没有写入,那就没办法执行下次循环再次读取数据
拓展: fgets()函数
char *fgets(char *str, int n, FILE *stream);
- 参数
str-- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
n-- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
stream-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
- 功能
从指定的流 stream 读取一行,并把它存储在str所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
//可以看出fgets的作用是读取我们指定流里的数据,因为我们上面代码指定的流是标准输入,指向的是当前终端,所以我们可以一直在终端输入,这和fgets函数是没有关系的,我们如果在同一个终端多输入几次,再到另一个终端一次一次输入就会发现,之前写入的数据还是在当前终端里一次一次的输出,这里也就说明了我们每次循环确实是只执行一次gets
2
3
4
5
6
7
8
9
10
# 12.3 运行结果及解释2
- 一下运行结果是在我们把上面代码的所有
printf()
里的/n 换行符
全都去掉后会发生的情况
- 这里我们先在终端1执行
./a
,会发现虽然我们没有管道文件,但是没有输出🟥里的文字,只有等我们到终端2 里执行./b
后才一起输出 - 这是是Linux的输出缓冲区所致。linux的标准缓冲区一般是行缓冲,即遇到换行才输出。打开进程b输出了没有换行的数据,是进程A的fgets函数刷新了输出缓冲区。,就是说这里的🟥里的输出其实是fgets函数输出的
# 十三、内存映射
# 1、内存映射图解
- 这里的进程地址空间就是虚拟地址空间,只不过虚拟地址空间最终会对应到物理内存
# 2、内存映射相关系统调用
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
1
2
3
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void * addr:NULL,(映射内存的首地址)由内核指定
- length :要映射的数据的长度,这个值不能为0.建议使用文件的长度。
获取文件的长度:stat函数 或者 lseek函数
- prot :对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。所以我们要使用一般这样写:
PROT_READ (读权限)或 PROT_READ | PROT_WRITE(读写权限)
- flags:
- MAP_SHARED :映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write,写时拷贝)
- fd :需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意;文件的大小不能为0,open指定的权限不能和 prot参数有冲突
如果prot:PROT_READ 那么open可以是:只读/读写
如果prot:PROT_READ | PROT_WRITE 那么open只能是:读写
(就是prot的权限必须要小于或等于open的权限)
-offset:偏移量,一般不用。因为必须指定的是4k的整数倍,0表示不偏移。
- 返回值:成功返回创建的内存的首地址
失败返回MAP_FAILED,(void*) -1
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length :要释放的内存的大小,要和mmap函数中length参数的值一样
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞的。
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3、内存映射实现父子进程间的通信
//mmap-parent-child-ipc.c
#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main()
{
// 1.打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END); // 获取文件的大小
// 2.创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 3.创建子进程
pid_t pid = fork();
if (pid > 0)
{
// 回收完子进程的资源后再执行父进程的代码
wait(NULL);
// 父进程
char buf[64];
// ptr 是void指针类型,这里我们要强转成 (char*)
strcpy(buf, (char *)ptr);
printf("read data : %s\n", buf);
}
else if (pid == 0)
{
// 子进程
strcpy((char *)ptr, "nihao a, son!!!");
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
注意这里只输出了
nihao a,son!!!
的原因:原型声明:char *strcpy(char* dest, const char *src); 头文件:#include <string.h> 和 #include <stdio.h> 功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间
1
2
3是因为子进程里使用了
strcpy((char *)ptr, "nihao a, son!!!");
,把一个字符串拷贝到了内存中,字符串是以"\0"字符结尾,而父进程又使用strcpy(buf, (char *)ptr);
将内存里的数据拷贝到buf里来进行打印,所以后续打印数据的时候,打印的是字符串,遇到"\0"就把前面的字符串取出来了,后面其实还有数据在内存中的。
# 4、内存映射实现无联系进程间的通信
# 5、内存映射实现文件拷贝
# 5.1 思路
// 使用内存映射实现文件拷贝的功能
/*
思路:
1.对原始的文件进行内存映射
2.创建一个新文件(拓展该文件)
3.把新文件的数据映射到内存中
4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存里
5.释放资源
*/
2
3
4
5
6
7
8
9
# 5.2 代码
//copy.c
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
// 1.对原始的文件进行内存映射
int fd = open("english.txt", O_RDWR);
if (fd == -1)
{
perror("open");
exit(0);
}
// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);
// 2.创建一个新文件(拓展该文件)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if (fd1 == -1)
{
perror("open");
exit(0);
}
// 对新创建的文件进行拓展
truncate("cpy.txt", len);
write(fd1, " ", 1);
// 3.分别做内存映射
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
if (ptr1 == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 内存拷贝
memcpy(ptr1, ptr, len);
// 释放资源
// 先打开的后释放,后打开的先释放,之所以这样是怕后面打开或创建的对前面的有依赖关系
munmap(ptr1, len);
munmap(ptr, len);
// 关闭也是先开后关
close(fd1);
close(fd);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 5.3 运行结果
可以看到创建了
cpy.txt
文件,如果我们打开该文件,会发现 english.txt 文件的内容被拷贝到了cpy.txt
里注意这个拷贝不能拷贝太大的文件,不然可能内存放不下
# 6、匿名映射
# 1、匿名映射含义
//匿名映射:不需要文件实体进行一个内存映射,只能用在有关联的进程的通信
/*
我们普通的内存映射是需要打开文件获取文件描述符然后磁盘文件的数据映射到内存里去,但是匿名映射不需要,而是在给flag 增加一个 MAP_ANONYMOUS 权限,文件描述符就传入 -1即可
*/
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
2
3
4
5
6
7
# 2、代码
//mmap-anon.c
/*
匿名映射:不需要文件实体进行一个内存映射, 只能用在有关联的进程的通信
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
// 1.创建匿名内存映射区
int len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 父子进程间通信
pid_t pid = fork();
if (pid > 0)
{
// 父进程
strcpy((char *)ptr, "hello,world");
wait(NULL);
}
else if (pid == 0)
{
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap");
exit(0);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 3、运行截图
- 注意:匿名通信只能用于有关联的进程间的通信
# 7、内存映射注意事项
如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(...); ptr++; 可以对其进行++操作 munmap(ptr, len); // 错误,因为ptr不再是首地址,所以要对ptr进行操作要先保存地址
1
2
3如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
- 错误,返回MAP_FAILED
- open()函数中的权限建议和prot参数的权限保持一致。(prot的权限要小于等于open的权限)
如果文件偏移量为1000会怎样?
- 偏移量必须是4K(一个分页大小)的整数倍,返回MAP_FAILED
mmap什么情况下会调用失败? - 第二个参数:length = 0 - 第三个参数:prot - 只指定了写权限(必须有读权限) - prot PROT_READ | PROT_WRITE 第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
可以open的时候O_CREAT一个新文件来创建映射区吗? - 可以的,但是创建的文件的大小如果为0的话,肯定不行 - 可以对新的文件进行扩展 - lseek() - truncate()
mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX"); mmap(,,,,fd,0); close(fd); // 映射区还存在,创建映射区的fd被关闭,没有任何影响。
1
2
3
4对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
- 4K(一个分页的大小,不同系统分页大小不一定一样)
- 越界操作操作的是非法的内存 -> 段错误