进程是一个独立的资源分配单元,不同进程之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信IPC
(Inter Process Communication
)。
进程间通信的目的:
进程间通信的方式分为一台主机上 的进程间的通信以及不同主机的进程间的通信。
不同主机进程间通信,类似常用的QQ,通过网络实现,常用的实现技术是Socket
。
同一主机进程间的通信分为:
匿名管道
、有名管道
、信号
等;消息队列
、共享内存
、信号量
等;接下来是关于匿名管道实现进程间通信的内容。
管道也叫匿名管道也叫无名管道,是UNIX系统IPC的最古老的形式,所有的UNIX系统都支持这种通信机制。
通常说的管道是匿名管道。
例如:统计一个目录中文件的数目命令: ls | wc -l
,为了执行该命令,shell创建了两个进程分别执行ls
和wc
。|
也称为管道符;
首先执行
ls
产生一个进程,会得到当前目录下的所有文件及文件夹,默认是输出到终端;
使用管道符|
把要输出到终端的内容通过管道传输给另一个进程wc
;默认wc
是从当前终端获取输入,通过管道符|
后变成从管道中获取输入;
如下图所示:
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同;
管道拥有文件的特质:读操作、写操作,但管道没有文件实体。匿名管道一般用在有关系的进程之间
,比如父子进程、兄弟进程之间等。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。管道中的数据结构是环形队列。
在管道中数据传递的方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
需要注意的是,匿名管道只能用于具有亲缘关系的进程间的通信;管道默认是阻塞的,如果管道中没有数据,read阻塞;如果管道满了,write阻塞
。
单工:如遥控器发送数据给电视机,只能是单向的;
双工:如打电话之间联系是双向的;双车道可以双向行驶
半双工:同一间,数据只能单向传递;如对讲机,同一时间只能单向联系;
从管道读取数据是一次性操作,数据一旦被读走,就从管道中被抛弃,释放空间以便写更多的数据,管道中无法使用lseek()
等函数随机访问数据。
前面了解到fork()创建的子进程,父进程和子进程共享文件描述符表。所以使用匿名管道实现进程间通信时,首先要创建管道,然后再创建子进程,这样父进程和子进程就可以共享管道描述符。
■ 创建匿名管道
#include
int pipe(int pipefd[2]);
使用示例:创建一个子进程,子进程和父进程之间相互发送和接收数据。
#include
#include
#include
#include
#include
int main()
{
// 在fork之前创建管道
int pipefd[2]={0};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
char buf[1024]={0};
if(pid > 0) // 父进程
{
printf("i am parent process , pid : %d\n",getpid());
// 父进程接收数据
while (1)
{
// 父进程从管道读数据
int len = read(pipefd[0],buf,sizeof(buf));
printf("parent receive : %s, pid : %d\n",buf,getpid());
// 父进程向管道写数据
char *str = "hello ,i am parent!";
write(pipefd[1],str,strlen(str));
sleep(2);
}
}
else if(pid == 0) // 子进程
{
printf("i am child process , pid : %d\n",getpid());
while (1)
{
// 子进程 向管道中写数据
char *str = "hello ,i am child!";
write(pipefd[1],str,strlen(str));
sleep(2);
// 子进程从管道读数据
int len = read(pipefd[0],buf,sizeof(buf));
printf("child receive : %s, pid : %d\n",buf,getpid());
}
}
return 0;
}
显示结果,子进程和父进程交替发送数据,首先是父进程接收到子进程发送的数据,然后子进程接收到父进程发送的数据:因为代码中是子进程先向管道中写入数据,父进程先从管道中读出数据。 程序中不能子进程和父进程都先去读,如果这样会一直阻塞到这里;也不能都先去写,因为可能会导致父进程读取到父进程写的数据,子进程读取到子进程写的数据
。
■ 查看管道缓冲大小
可以使用命令 ulimit -a
查看管道缓冲大小;
还可以使用函数 fpathconf()
查看管道缓冲大小
#include
long fpathconf(int fd, int name);
函数参数:
man fpathconf
。函数查看管道缓冲区大小示例:
#include
#include
#include
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;
}
显示结果:
使用管道时需要注意几种特殊情况:(假设都是阻塞I/O操作下)
SIGPIPE
,会导致进程异常终止;总结就是:
读
管道时:
- 管道中有数据,read读取会返回实际读到的字节数;
- 管道中没有数据:
- 写端被全部关闭,read读取返回0;
- 写端没有被完全关闭,read读取会阻塞等待,直到管道中有了数据;
写
管道时:
- 管道读端全部关闭,进程异常终止,向管道中写数据的进程收到SIGPIPE信号;
- 管道读端没有全部关闭:
- 管道已满,write写阻塞,直到管道中有空位置;
- 管道没有满,write将把数据写入,并返回实际写入的字节数;
/*
* 实现 ps aux | grep root类似功能
* 需要用到父子进程间通信
*
* 子进程执行 ps aux,子进程结束后将得到的数据发送给父进程
* 父进程获取到数据,过滤
*
* 使用匿名管道 pipe()函数通信,使用exec函数族执行 ps aux命令
* 获取到子进程的标准输出,子进程的标准输出重定向到父进程即管道的写端,dup2
*
*/
#include
#include
#include
#include
#include
#include
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;
// int len = read(fd[0],buf,sizeof(buf)-1);
while ((len = read(fd[0], buf, sizeof(buf) - 1)) > 0)
{
printf("%s", buf);
memset(buf, 0, sizeof(buf));
}
wait(0); // wait() 回收子进程资源
}
else if (pid == 0)
{
// 子进程中执行 ps aux
// 文件描述符重定向,把标准输出重定向到管道的写端
// 关闭读端
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
// perror("execlp");
exit(0);
}
else
{
perror("fork");
exit(0);
}
return 0;
}
可以使用
fcntl()
设置管道描述符为非阻塞状态。
int flag = fcntl(fd[0],F_GETFL); // F_GETFL 获取文件描述符状态的标记
flag |= O_NOBLOCK; // 修改flag值
fcntl(fd[0],F_SETFL,flag); // 设置新的flag,设置为非阻塞
示例:
#include
#include
#include
#include
#include
#include
/**
* 设置管道非阻塞
* 使用函数fcntl()
* int flag = fcntl(fd[0],F_GETFL); // F_GETFL 获取文件描述符状态的标记
* flag |= O_NOBLOCK; // 修改flag值
* fcntl(fd[0],F_SETFL,flag); // 设置新的flag
*/
int main()
{
// 在fork之前创建管道
int pipefd[2] = {0};
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
char buf[1024] = {0};
if (pid > 0) // 父进程
{
printf("i am parent process , pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
int flag = fcntl(pipefd[0], F_GETFL); // F_GETFL 获取文件描述符状态的标记
flag |= O_NONBLOCK; // 修改flag值
fcntl(pipefd[0], F_SETFL, flag); // 设置新的flag,非阻塞模式
// 父进程接收数据
while (1)
{
// 父进程从管道读数据
sleep(1);
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d\n", len);
printf("parent receive : %s, pid : %d\n", buf, getpid());
memset(buf, 0, sizeof(buf));
}
}
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;
}
显示结果:
i am parent process , pid : 49939
i am child process , pid : 49940
len : 18
parent receive : hello ,i am child!, pid : 49939
len : -1
parent receive : , pid : 49939
len : -1
parent receive : , pid : 49939
len : -1
parent receive : , pid : 49939
len : 18
parent receive : hello ,i am child!, pid : 49939
len : -1
parent receive : , pid : 49939