实验9

1.1

​ 操作系统中进程通信的作用是允许不同的进程之间交换信息和共享资源,以实现协同工作和数据共享。这有助于多任务处理和分布式系统的有效管理。进程通信可通过各种机制实现,包括共享内存、消息传递、管道等。通过进程通信,不同的进程可以协调完成任务、共享数据,提高系统的整体效率。


1.2

进程间通信有多种方式,其中一些主要的方式包括:

  1. 管道(Pipes):单向通信,通常用于具有亲缘关系的父子进程间的通信。
  2. 消息队列(Message Queues):允许通过消息进行双向通信,消息被放入队列,其他进程可以读取队列中的消息。
  3. 信号(Signals):用于通知进程发生了特定事件,例如中断或错误。信号机制比较简单,但不能传递大量数据。
  4. 共享内存(Shared Memory):允许多个进程直接访问同一块物理内存,效率较高。需要进行同步以避免冲突。
  5. 套接字(Sockets):提供了网络通信的一种方式,也可用于本地进程通信。适用于不同计算机或同一计算机上的进程。
  6. 信号量(Semaphores):用于进程间同步和互斥,确保在共享资源上的互斥访问。

每种通信方式都有其适用的场景和特点,选择通信方式通常取决于进程间需要传递的数据量、通信的频率、同步需求等因素。


1.3

1. fd1 = open(“a.txt”, O_RDONLY);

打开文件”a.txt”以只读方式,fd1将被赋予相应的文件描述符值。

2. fd2 = open(“b.txt”, O_WRONLY);

打开文件”b.txt”以只写方式,fd2将被赋予相应的文件描述符值。

3. fd3 = dup(fd1);

使用dup复制fd1,返回一个新的文件描述符,该描述符是尚未使用的最小值。fd3将获得这个新的文件描述符。

4. fd4 = dup2(fd2, 0);

使用dup2复制fd2到0(标准输入),并返回0。fd4将获得这个新的文件描述符。

假设当前终端没有打开任何正常文件,那么:

  1. fd1: 打开”a.txt”,因此其值为 a.txt 的文件描述符。
  2. fd2: 打开”b.txt”,因此其值为 b.txt 的文件描述符。
  3. fd3: 使用dup复制fd1,返回的是尚未使用的最小文件描述符,因此 fd3 为 a.txt 的文件描述符。
  4. fd4: 使用dup2将fd2复制到0(标准输入),返回的是指定的文件描述符,因此 fd4 为 b.txt 的文件描述符。

2.1

代码:

1
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.c

#include<stdio.h> //printf
#include<fcntl.h> //open
#include<unistd.h> //dup fflush close

int main(){
// 打开文件c.txt以只写方式
int file_fd = open("c.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);

// 备份标准输出文件描述符
int stdout_backup = dup(1);

// 将标准输出重定向到文件c.txt
dup2(file_fd,1);

// 第一次输出到文件c.txt
printf("Hello Linux\n");

// 刷新打开的流
fflush(NULL);

// 还原标准输出,文件标识符1给stdout_backup,1不再指向file_fd,即还原了
dup2(stdout_backup,1);

// 第二次输出到屏幕上
printf("Hello Linux\n");

// 刷新打开的流
fflush(NULL);

// 关闭文件描述符
close(file_fd);

return 0;
}

这个程序首先打开文件”c.txt”,然后使用dup2将标准输出重定向到该文件。接着执行printf语句,将输出写入文件。之后,通过dup2将标准输出还原到原来的文件描述符,再次执行printf语句,将输出显示在屏幕上。最后,关闭文件描述符。

在使用 dup 或 dup2 函数进行文件描述符复制时,还原标准输出指的是将原来的标准输出(文件描述符1)恢复到其先前的状态。在涉及重定向标准输出的操作中,程序可能会将标准输出指向其他文件描述符,比如一个文件或管道。为了确保程序后续的输出会回到标准输出,需要在适当的时候还原标准输出。

举例来说,在之前的一个回答中,有这样一段代码:

1
2
3
4
int stdout_backup = dup(1);   // 备份标准输出文件描述符
dup2(file_fd, 1); // 将标准输出重定向到文件c.txt
// ... 执行一些操作,比如printf
dup2(stdout_backup, 1); // 还原标准输出

这里,stdout_backup = dup(1) 备份了标准输出的文件描述符,然后 dup2(file_fd, 1) 将标准输出重定向到文件。之后,通过 dup2(stdout_backup, 1) 将标准输出还原到之前备份的文件描述符,以确保后续的输出会回到标准输出。这个还原操作是为了维持程序的输出正常流程。

这里,dup(1)创建了标准输出文件描述符的副本,并将其存储在stdout_backup中。稍后,如果标准输出被重定向(例如,使用dup2将其指向一个文件),dup2(stdout_backup, 1)通过将备份的文件描述符复制回标准输出来进行还原。

结果:

2.2

代码:

1
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
#include<stdio.h> //perror
#include<stdlib.h>
#include<unistd.h> //exit fork close
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h> //strlen

#define BUFFER_SIZE 50

int main(){
// 创建管道
int pipe_fd[2];
if(pipe(pipe_fd) == -1){
perror("Pipe creation falied");
exit(EXIT_FAILURE);
}

// 创建子进程
pid_t child_pid = fork();

if(child_pid == -1){
perror("Fork failed");
exit(EXIT_FAILURE);
}

if(child_pid > 0){
// 父进程
//close(pipe_fd[0]);// 关闭读取端
char message[] = "Hello World!";
printf("Parent sends:%s\n",message);

// 将字符串写入管道
write(pipe_fd[1],message,strlen(message) + 1);

// 等待子进程结束
wait(NULL);

// 读取子进程传回的消息
char reversed_message[BUFFER_SIZE];
read(pipe_fd[0],reversed_message,sizeof(reversed_message));

printf("Parent receives:%s\n",reversed_message);

close(pipe_fd[1]); // 关闭写入端
close(pipe_fd[0]); // 关闭读取端
}else{
// 子进程
//close(pipe_fd[1]);// 关闭写入端
char received_message[BUFFER_SIZE];

// 从管道中读取父进程发送的消息
read(pipe_fd[0],received_message,sizeof(received_message));
printf("Child receives:%s\n",received_message);

// 将字符串倒序
int length = strlen(received_message);
for(int i = 0;i < length/2;i++){
char temp = received_message[i];
received_message[i] = received_message[length-i-1];
received_message[length-i-1] = temp;
}

// 在倒序字符串后附加子进程号
pid_t child_pid = getpid();
snprintf(received_message + length, BUFFER_SIZE - length, "%d", child_pid);


// 将处理后的消息写回管道
write(pipe_fd[1], received_message, strlen(received_message) + 1);

close(pipe_fd[0]); // 关闭读取端
close(pipe_fd[1]); // 关闭写入端
}
}

父子进程不能关闭写入端和读取端,否则父进程无法接收子进程的字符串

结果:

2.3

思路:进程1–>pipe_ps_grep–>进程2–>pipe_grep_wc->进程3

代码:

1
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
91
92
93
94
95
96
97
98
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
int pipe_ps_grep[2];
int pipe_grep_wc[2];

// 创建第一个管道
if (pipe(pipe_ps_grep) == -1) {
perror("pipe_ps_grep");
exit(EXIT_FAILURE);
}

pid_t ps_child_pid;

// 创建第一个子进程执行 ps -uax
if ((ps_child_pid = fork()) == -1) {
perror("fork_ps");
exit(EXIT_FAILURE);
}

if (ps_child_pid == 0) {
/*使用STDOUT_FILENO文件描述符指向pipe_ps_grep[1]:写管道,一般情况下对应的文件描述符是4,
需要关闭原文件描述符*/
dup2(pipe_ps_grep[1], STDOUT_FILENO);
//关闭原文件描述符
close(pipe_ps_grep[0]);
close(pipe_ps_grep[1]);
execlp("ps", "ps", "-uax", NULL);
perror("execlp_ps");
exit(EXIT_FAILURE);
}

// 创建第二个管道
if (pipe(pipe_grep_wc) == -1) {
perror("pipe_grep_wc");
exit(EXIT_FAILURE);
}

pid_t grep_child_pid;

// 创建第二个子进程执行 grep root
if ((grep_child_pid = fork()) == -1) {
perror("fork_grep");
exit(EXIT_FAILURE);
}

if (grep_child_pid == 0) {
//读pipe_ps_grep[0]端
dup2(pipe_ps_grep[0], STDIN_FILENO);
close(pipe_ps_grep[0]);
close(pipe_ps_grep[1]);

//写pipe_grep_wc[1]端
dup2(pipe_grep_wc[1], STDOUT_FILENO);
close(pipe_grep_wc[0]);
close(pipe_grep_wc[1]);

execlp("grep", "grep", "root", NULL);
perror("execlp_grep");
exit(EXIT_FAILURE);
}

// 关闭第一个管道在父进程中未使用的部分
close(pipe_ps_grep[0]);
close(pipe_ps_grep[1]);

// 创建第三个子进程执行 wc -l
pid_t wc_child_pid;
if ((wc_child_pid = fork()) == -1) {
perror("fork_wc");
exit(EXIT_FAILURE);
}

if (wc_child_pid == 0) {
//读pipe_grep_wc[1]端
dup2(pipe_grep_wc[0], STDIN_FILENO);
close(pipe_grep_wc[0]);
close(pipe_grep_wc[1]);

execlp("wc", "wc", "-l", NULL);
perror("execlp_wc");
exit(EXIT_FAILURE);
}

// 关闭第二个管道在父进程中未使用的部分
close(pipe_grep_wc[0]);
close(pipe_grep_wc[1]);

// 等待所有子进程结束
waitpid(ps_child_pid, NULL, 0);
waitpid(grep_child_pid, NULL, 0);
waitpid(wc_child_pid, NULL, 0);

return 0;
}

execlp是一个在Unix/Linux系统中用于执行可执行文件的系统调用。它会取代当前进程的内存映像,加载并执行指定的可执行文件。

具体来说,execlp函数的作用是在调用进程中执行一个新的程序。它接受可执行文件的路径,以及一系列的命令行参数,最后一个参数必须是NULL,用于表示参数列表的结束。

在上述C程序的例子中,execlp("ps", "ps", "-uax", NULL)用于执行ps -uax命令,而execlp("grep", "grep", "root", NULL)用于执行grep root命令。这两个函数调用使得子进程加载并执行了这两个命令,替代了原有的进程内存映像。

这是一个使用C语言编写的程序,通过无名管道、fork函数和exec函数实现了模拟命令ps -uax | grep root | wc -l的功能。下面是对程序的解释:

  1. 管道的创建:
    pipe(pipe_ps_grep): 创建了一个管道,用于连接ps进程和grep进程。

    pipe(pipe_grep_wc): 创建了另一个管道,用于连接grep进程和wc进程。

  2. ps进程的创建:

    ps_child_pid = fork(): 创建了第一个子进程,该子进程执行ps -uax命令。

    在子进程中,使用dup2ps的标准输出连接到pipe_ps_grep的写入端口,并通过execlp执行ps命令。

  3. grep进程的创建:

    grep_child_pid = fork(): 创建了第二个子进程,该子进程执行grep root命令。

    在子进程中,使用dup2ps的标准输出连接到pipe_ps_grep的读取端口,将grep的标准输出连接到pipe_grep_wc的写入端口,并通过execlp执行grep命令。

  4. wc进程的创建:

    wc_child_pid = fork(): 创建了第三个子进程,该子进程执行wc -l命令。

    在子进程中,使用dup2grep的标准输出连接到pipe_grep_wc的读取端口,并通过execlp执行wc命令。

  5. 管道的关闭:

    父进程关闭了在子进程中未使用的管道部分。

    dup2函数用于复制文件描述符,将一个文件描述符指向另一个文件描述符。当你使用dup2将某个文件描述符指向一个管道的端口时,通常需要关闭原始的文件描述符,因为它已经被复制到了新的文件描述符。

    在管道的使用场景中,dup2的典型用法是将标准输入或标准输出重定向到管道的读取端或写入端。一旦重定向完成,原始的标准输入或标准输出就变得多余了,因此可以关闭它。

  6. 等待子进程结束:

    使用waitpid函数等待所有子进程执行完毕。

​ 这样,通过使用管道连接多个子进程,实现了ps -uax | grep root | wc -l命令的功能。父进程负责创建子进程,并确保正确的管道连接。

结果:

2.4

代码:

1
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
#include <stdio.h>
#include <stdlib.h>

int main() {
//打开一个管道(只读),并执行ps -uax
FILE *pipe_ps = popen("ps -uax", "r");
if (pipe_ps == NULL) {
perror("popen ps");
exit(EXIT_FAILURE);
}

//打开一个管道(只写),并执行ps -uax
FILE *pipe_grep = popen("grep root", "w");
if (pipe_grep == NULL) {
perror("popen grep");
exit(EXIT_FAILURE);
}

char buffer[4096];
size_t bytesRead;

// 从ps的输出读取数据并写入到grep
while ((bytesRead = fread(buffer, 1, sizeof(buffer), pipe_ps)) > 0) {
fwrite(buffer, 1, bytesRead, pipe_grep);
//size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
//size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
//size--这是要被写入的每个元素的大小,以字节为单位。
}

// 关闭管道
if (pclose(pipe_ps) == -1) {
perror("pclose ps");
exit(EXIT_FAILURE);
}

if (pclose(pipe_grep) == -1) {
perror("pclose grep");
exit(EXIT_FAILURE);
}

return 0;
}

这个程序使用两个popen调用分别执行ps -uax和grep root,并将它们通过管道连接起来。然后,通过fread和fwrite从ps的输出读取数据并写入到grep的输入。最后,使用pclose关闭两个管道。

这样,你就完成了模拟ps -uax | grep root的操作,而无需手动创建管道和子进程

结果:

popen是一个C标准库函数,用于在一个新的进程中打开一个管道并执行一个 shell 命令。它返回一个文件指针,可以用于读取或写入与子进程关联的管道。

函数签名如下:

1
FILE *popen(const char *command, const char *type);
  • command 参数是要执行的 shell 命令字符串。
  • type 参数是一个字符串,用于指定打开管道的方式。常用的值包括 "r"(只读)和 "w"(只写)。

返回值是一个文件指针,可以使用标准文件操作函数(如 freadfwritefclose 等)来读取或写入与子进程关联的管道。

例如,popen("ps -uax", "r") 会执行 ps -uax 命令,并返回一个文件指针,可以用于读取该命令的标准输出。

pclose 用于关闭由 popen 打开的管道,并等待相关的子进程结束。它的函数签名如下:

1
int pclose(FILE *stream);
  • streampopen 返回的文件指针。

返回值是子进程的终止状态,或者 -1 表示有错误发生。

请注意,popenpclose 在处理大量数据时可能存在一些限制,因为它们依赖于标准I/O缓冲区。如果你需要更灵活的控制,或者处理大量数据,可能需要使用其他机制,比如forkpipe

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2024 CoffeeLin
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信