1.概念扫盲
1.1 IPC的概念
IPC (Inter-Process Communication) 是指进程间通信的技术和机制。在操作系统中,不同的进程可能需要相互交换信息、共享资源或协调工作,而 IPC 提供了一种机制,使得这些进程可以进行有效的通信和数据交换。
IPC 提供了各种通信方式和机制,包括管道、消息队列、共享内存、信号量、套接字等。这些机制允许进程之间进行数据传输、同步操作、共享资源等,以实现进程之间的协作和通信。
通过使用 IPC,进程可以在共享资源的基础上进行协同工作,实现数据共享、任务协作和系统集成。IPC 在操作系统和网络编程中起着重要的作用,它使得不同进程之间能够相互通信和协作,提高了系统的灵活性、可扩展性和效率。
2. 管道
在Linux C语言中,管道(Pipeline)是一种进程间通信的机制,可以用于在C程序中实现进程间的数据传输,管道是通过操作系统提供的系统调用函数 pipe
来创建的。pipe
函数创建一个管道,返回两个文件描述符,一个用于读取管道数据,另一个用于写入管道数据。这两个文件描述符分别被称为管道的读端和写端。
管道有以下两种
1、匿名管道 pipe:适用于亲缘关系进程间的、一对一的通信
2、具名管道 fifo :适用于任何进程间的一对一、多对一的通信
2.1匿名管道
管道的创建方法如下
#include <unistd.h>
int pipe( int fd[2] );
将一个有两个元素的整形数组地址传入到pipe函数中,该数组即可被初始化,初始化后的数组中即包含了两个文件描述符,分别代表着管道的读写端。其中fd[0]代表着文件的读端。fd[1]代表着文件的写端。
下面是一个父进程向子进程发送信息,子进程接收信息并显示出来,如果父进程发送quit则子进程退出的代码例程:
#define BUFFERMAX 1024
int main()
{
int pipefd[2];
int ret = pipe( pipefd );
if(ret == -1)
{
perror("pipe");
}
pid_t pid_fd = fork();
int status = 0;
if(pid_fd == 0) //子 == 进程2
{
char getmsg[BUFFERMAX];
while (1)
{
close(pipefd[1]);
bzero(getmsg,BUFFERMAX);
read(pipefd[0],getmsg,BUFFERMAX);
printf("读到的字符串是%s\n",getmsg);
if( strstr(getmsg,"quit") != NULL)
{
printf("找到了子串,子进程退出\n");
exit(0);
}
}
}
else if(pid_fd > 0) //父 == 进程1
{
char inputmsg[BUFFERMAX];
while (1)
{
close(pipefd[0]);
bzero(inputmsg,BUFFERMAX);
printf("请输入要发送的字符串\n");
scanf("%s",inputmsg);
write(pipefd[1],inputmsg,BUFFERMAX);
SLEEP_MS(10);
if( strstr(inputmsg,"quit") != NULL)
{
wait(&status);
break;
}
}
}
}
2.2 管道的读写特性
2.2.1 读者: 对管道拥有读权限的进程
在理解读取管道时的阻塞和非阻塞行为时,需要考虑管道的读写端的状态和行为。
当有写者存在时,也就是有进程向管道中写入数据,读者在读取管道时的行为如下:
1、如果管道中有数据可读,读者会正常读取数据,并将其从管道中移除。
2、如果管道中没有数据可读,读者会进入阻塞状态,等待写者向管道中写入数据。
当没有写者存在时,也就是没有进程向管道中写入数据,读者在读取管道时的行为如下:
3、如果管道中有数据可读,读者会正常读取数据,并将其从管道中移除。
4、如果管道中没有数据可读,读者不会进入阻塞状态,而是立即返回,返回值为0,表示没有读取到任何数据。
2.2.2 写者: 对管道拥有写权限的进程
当有读者存在时,也就是有进程从管道中读取数据,写者在写入管道时的行为如下:
1、如果管道的缓冲区未满,写者会正常写入数据,并将其存储在管道中。
2、如果管道的缓冲区已满,写者会进入阻塞状态,等待读者从管道中读取数据,以腾出空间供写入。
当没有读者存在时,也就是没有进程从管道中读取数据,写者在写入管道时的行为如下:
3、无论缓冲区是否已满,写者会立即收到 `SIGPIPE` 信号,该信号表示管道的写端已经关闭或无法写入数据。通常情况下,写者需要正确处理该信号,以避免程序异常终止。
下面演示一下当写端已经关闭时,read会立即返回的现象:我们基于上面的代码做了一些增添,让父进程写入一次数据后便直接退出(即此时已经没有写者存在)
int main()
{
int pipefd[2];
int ret = pipe( pipefd );
if(ret == -1)
{
perror("pipe");
}
pid_t pid_fd = fork();
int status = 0;
if(pid_fd == 0) //子 == 进程2
{
char getmsg[BUFFERMAX];
while (1)
{
close(pipefd[1]);
bzero(getmsg,BUFFERMAX);
read(pipefd[0],getmsg,BUFFERMAX);
printf("读到的字符串是%s\n",getmsg);
if( strstr(getmsg,"quit") != NULL)
{
printf("找到了子串,子进程退出\n");
exit(0);
}
}
}
else if(pid_fd > 0) //父 == 进程1
{
char inputmsg[BUFFERMAX];
while (1)
{
close(pipefd[0]);
bzero(inputmsg,BUFFERMAX);
printf("请输入要发送的字符串\n");
scanf("%s",inputmsg);
write(pipefd[1],inputmsg,BUFFERMAX);
if( strstr(inputmsg,"quit") != NULL)
{
wait(&status);
break;
}
printf("父进程退出,此时无写者存在\n");
exit(0);
}
}
}
此时显示结果如下,可以看到子进程不断执行printf语句,未被阻塞。
请输入要发送的字符串
123
父进程退出,此时无写者存在
读到的字符串是123
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# 读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
思考1:那么read函数又是怎么知道写端被关闭了呢?
读端或写端关闭的情况通常有以下几种:
1. 写端关闭:当写端所属的进程或文件描述符调用了 `close` 函数关闭了管道的写端,我们可以认为写端已关闭。
2. 读端关闭:当读端所属的进程或文件描述符调用了 `close` 函数关闭了管道的读端,我们可以认为读端已关闭。
3. 进程终止:当与管道相关联的进程终止时,操作系统会自动关闭进程的所有打开文件描述符,包括管道的读端和写端。因此,当与管道相关的进程终止时,我们可以认为管道的读端和写端都已关闭。
4. 程序执行完毕:在某些情况下,写端或读端可能是由同一个程序在不同的时刻打开和关闭的。当程序执行完毕,也就是主程序 `main` 函数结束时,操作系统会自动关闭程序打开的所有文件描述符,包括管道的读端和写端。
需要注意的是,关闭管道的读端和写端并不是互斥的。当读端和写端都关闭后,管道才被认为完全关闭,否则管道仍然是部分打开状态。在读端和写端关闭后,进程仍然可以继续操作打开的另一端。
而read函数并不直接判断是否有写者存在。它的行为是基于管道的状态来确定是否阻塞,以及何时返回。如果没有写者存在,读者会在读取到管道中的所有数据后,最终读取到 0 字节,并返回表示管道结束的标志。这是因为在管道中,读者读取到所有数据后,若写端关闭,将会收到一个 EOF(文件结束)信号,从而读取返回 0 字节。
思考2:在第一个例程中,子进程里使用了close关闭了写端,为什么read依然会被阻塞?
在子进程中关闭了管道的写端,这意味着子进程无法再向管道写入数据。然而,关闭写端并不会立即导致 read
函数阻塞返回,因为管道的读端仍然打开着。
在管道中,读端的阻塞行为受到写端是否关闭和管道中是否有数据的影响。当管道中有数据可读时,read
函数会正常读取数据并返回。但当管道中没有数据时,read
函数会阻塞等待,直到有数据可读或者写端关闭。
在代码中,子进程在每次循环开始时关闭了管道的写端。但由于父进程在循环中不断向管道写入数据,所以管道中始终有数据可读。因此,子进程的 read
函数不会被阻塞,它会一直读取到父进程写入的数据。
如果想在子进程中的 read
函数阻塞等待,可以考虑在父进程中关闭管道的写端,这样子进程在读取完父进程写入的数据后,当再次尝试读取时会发现写端已经关闭,此时 read
函数会返回0,表示管道结束。
2.2.3 通过fcntl改变管道的阻塞状态
要改变文件的阻塞状态,可以使用 F_SETFL
命令并结合使用 O_NONBLOCK
标志。示例如下:
int flags = fcntl(fd, F_GETFL); // 获取文件的状态标志
flags |= O_NONBLOCK; // 设置非阻塞标志
fcntl(fd, F_SETFL, flags); // 设置文件的状态标志
完整代码如下:
#define BUFFERMAX 1024
int main()
{
int pipefd[2];
int ret = pipe( pipefd );
if(ret == -1)
{
perror("pipe");
}
pid_t pid_fd = fork();
int status = 0;
if(pid_fd == 0) //子 == 进程2
{
char getmsg[BUFFERMAX];
int flags = fcntl(pipefd[0], F_GETFL); // 获取文件的状态标志
flags |= O_NONBLOCK; // 设置非阻塞标志
fcntl(pipefd[0], F_SETFL, flags); // 设置文件的状态标志
while (1)
{
close(pipefd[1]);
bzero(getmsg,BUFFERMAX);
read(pipefd[0],getmsg,BUFFERMAX);
printf("读到的字符串是%s\n",getmsg);
if( strstr(getmsg,"quit") != NULL)
{
printf("找到了子串,子进程退出\n");
exit(0);
}
}
}
else if(pid_fd > 0) //父 == 进程1
{
char inputmsg[BUFFERMAX];
while (1)
{
close(pipefd[0]);
bzero(inputmsg,BUFFERMAX);
printf("请输入要发送的字符串\n");
scanf("%s",inputmsg);
write(pipefd[1],inputmsg,BUFFERMAX);
// SLEEP_MS(20000);
if( strstr(inputmsg,"quit") != NULL)
{
wait(&status);
break;
}
}
}
}
现象是子进程不断 printf “读到的字符串是”
2.2.4 练习
编程实现下述命令的执行效果,查看系统进程列表中的指定进程信息:
gec@ubuntu:~$ ps ajx | grep 'xxx' --color
253
gec@ubuntu:~$
使用dup2重定向,将写入端重定向到STDOUT缓冲区,将读端重定向到STDIN缓冲区,再使用相关shell命令即可。完整代码如下:
#define BUFFERMAX 4096
int main(int argc, char *argv[])
{
int pipefd[2]; //读0写1
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
}
pid_t pid_fd = fork();
int status = 0;
if(pid_fd == 0) //子 == 进程2
{
dup2(pipefd[0],STDIN_FILENO);
printf("子进程执行ing\n");
execlp("grep", "grep", argv[1],"--color", NULL);
perror("execlp");
}
else if(pid_fd > 0) //父 == 进程1
{
dup2(pipefd[1],STDOUT_FILENO);
execlp("ps", "ps", "ajx",NULL);
perror("execlp");
SLEEP_MS(10);
wait(&status); //回收子进程
}
}
效果如下:
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# gcc test.c
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# ./a.out bash
子进程执行ing
2369 2531 2531 2531 ? -1 Ss 0 0:00 bash
2695 3530 3530 3530 pts/7 5486 Ss 0 0:00 /bin/bash --init-file /root/.vscode-server/bin/441438abd1ac652551dbe4d408dfcec8a499b8bf/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
5486 5487 5486 3530 pts/7 5486 S+ 0 0:00 grep bash --color
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道#