进程是一个独立的资源分配单元,不同的进程之间的资源是独立的。没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等。因此需要进程间通信。
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送信息,通知它发生了某件事情。(如结束进程需要通知父进程)
- 资源共享:多个进程之间共享同样的资源,需要内核提供互斥和同步机制
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
无名管道有以下的特点
- 半双工:数据在同一时刻只能有一个流向。
- 数据只能从管道的一段写入,从另一端读出
- 写入管道的数据遵循先入先出的规则
- 管道所传送的数据是无格式的,这要求管道读出方与写入方必须实现约定好数据的格式。比如多少字节算一个消息等
- 管道不是普通文化,不属于某个文件系统,其只存在于内存中。
- 管道在内存中对应一个缓冲区,不同的系统大小不一定相同。
- 从管道读取数据是一次性操作,数据一旦被读走,它就从管道被抛弃,释放空间以便写更多的数据
- 无名管道没有名字,只能在有公共祖先的进程之间使用。(比如:父子进程或者兄弟进程)
#include
#define _GNU_SOURCE
#include
int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);
/*
参数:
pipefd[2],两个文件描述符 pipefd[0]代表读的文件描述符,pipefd[1]代表写的文件描述符
返回值:成功返回0,失败-1
*/
使用实例
#include
#include
int main()
{
int fd[2];
//管道需要在子进程创建前开辟好,这样父子进程都可以得到pipe两端的文件描述符
pipe(fd);
pid_t pid=fork();
if(pid==0){
char buf[]="hello world";
write(fd[1],buf,sizeof(buf));
}
else if(pid>0){
char buff[25]={0};
//read的返回值为读取缓冲区的大小
//read是阻塞等待的,因此不会出现僵尸进程和孤儿进程
int ret=read(fd[0],buff,sizeof(buff));
if(ret>0){
write(STDOUT_FILENO,buff,ret);
}
}
return 0;
}
//输出: hello world
#include
#include
int main()
{
int fd[2];
//管道需要在子进程创建前开辟好,这样父子进程都可以得到pipe两端的文件描述符
pipe(fd);
pid_t pid=fork();
if(pid==0){
//需要使用dup2修改文件描述符的指向
//将标准输出重定向到写端;
dup2(fd[1],STDOUT_FILENO);
//执行ps指令,使用execlp函数
execlp("ps","ps","aux",NULL);
}
else if(pid>0){
//标准输入重定向到读端
dup2(fd[0],STDIN_FILENO);
sleep(2);
//grep也是阻塞等待函数,会继续等待输入
execlp("grep","grep","bash",NULL);
}
return 0;
}
但是上面会出现僵尸进程,为什么会出现僵尸进程?
我们输入grep bash命令,也会发现出现了阻塞。接下来进一步讲解管道的特性:
**由于进程是在创建子进程之前创建的,因此子进程和父进程都掌握了fd[0]和fd[1];**对于上面,由于父进程也掌握了写端,认为我们没有关闭写端,而且没有数据。所以grep就会发生阻塞。因此我们可以关闭写端或者fcntl更改为非阻塞。
#include
#include
int main()
{
int fd[2];
//管道需要在子进程创建前开辟好,这样父子进程都可以得到pipe两端的文件描述符
pipe(fd);
pid_t pid=fork();
if(pid==0){
//关闭子进程的读端
close(fd[0];)
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);
}
else if(pid>0){
//关闭父进程的写端,这样父进程就无法掌握写端,grep就无法发生阻塞
close(fd[1]);
/*
也可以使用fcntl设置为非阻塞,fcntl三板斧
int flags=fcntl(fd[1],F_GETFL); //得到文件的flag
flags|=O_NONBLOCK; //flag|=0_NONBLOCK
fcntl(fd[1],F_SETFL,flags);
*/
dup2(fd[0],STDIN_FILENO);
sleep(2);
execlp("grep","grep","bash",NULL);
}
return 0;
}
假定父进程实现ls,子进程实现wc。ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc –l 正常应该从stdin读取数据,但此时会从管道的读端读。
#include
#include
int main(){
int fd[2];
pipe(fd);
pid_t pid=fork();
if(pid==0)
{
//文件描述符重定向
//先关子进程的写端,否则会发生阻塞
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","l",NULL);
}
else if(pid>0)
{
//文件描述符重定向
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
}
}
可以使用 ulimit -a命令来查看当前系统中创建管道文件所对应的内核缓冲区大小通常为:
pipe size (512bytes,-p)8
也可以使用fpathconf函数。成功返回管道的大小,失败返回-1.
#include
long fpathconf(int fd, int name);
/*
参数说明:
fd:管道的fd[0];
name:是宏定义,如果查看管道的大小使用 _PC_PIPE_BUF
*/
优点:简单,相比信号,套接字实现进程间通信,简单很多。
缺点:1. 只能单向通信,双向通信需建立两个管道。
2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。
FIFO被称为命名管道,以区分管道(pipe)。无名管道pipe只能用于"有血缘关系"的进程。但是通过FIFO,不相关的进程也可以交换数据
FIFO是linux基础文件类型中的一种,但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道。**属于管道伪文件。**各进程可以打开这个文件进行read/write,实际上是在读写内核通道。
创建方式
创建方式有两种:
第一种是bash命令 mkfifo 管道名字
第二种是使用库函数。:int mkfifo(const char *pathname, mode_t mode);
pathname表示管道名,mode是管道权限
$ mkfifo myfifo
$ ls -lrt
fiforead.c,该文件用于读取FIFO管道的内容
int main(int argc,char*argv[])
{
if(argc!=2)
{
printf("./a.out fifoname\n");
return -1;
}
int fd=open(argv[1],O_RDONLY);
char buf[256];
int ret=0;
while(1)
{
//刷新缓冲区
memset(buf,0x00,sizeof(buf));
int ret=read(fd,buf,sizeof(buf));
if(ret>0)
{
printf("%s\n",buf);
}
}
close(fd);
return 0;
}
fifowrite.c,用于向FIFO文件中写入内容
int main(int argc,char*argv[])
{
if(argc!=2)
{
printf("./a.out fifoname\n");
return -1;
}
int fd=open(argv[1],O_WRONLY);
char buf[256];
int num=1;
while(1)
{
memset(buf,0x00,sizeof(buf));
sprintf(buf,"hello world%04d",num++);
//将数据写入缓冲区
write(fd,buf,sizeof(buf));
sleep(1);
}
close(fd);
return 0;
}
bash命令
$ gcc fiforead.c -o fiforead
$ gcc fifowrite.c -o fifowrite
$ ./fiforead myfifo
$ ./fifowrite mufifo
在上面两个.c文件中分别添加
printf("write open begin...\n");
int fd=open(argv[1],O_WRONLY);
printf("write open end ...\n");
printf("read open begin...\n");
int fd=open(argv[1],O_RDONLY);
prinft("read open end...\n");
我们执行语句
$ ./fiforead myfifo
出现这个现象的原因是:read是一个阻塞函数,会一直等待FIFO的write一端写入数据。我们执行写入端程序。
阻塞被解除
原理图片
mmap是直接操作内存,是进程间通信速度最快的方式。
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
/*
参数说明
addr:以前需要传递一个映射到的内存地址,现在只需传递NULL即可
length mmap映射的部分的长度。
prot:端口的权限描述
PROT_READ 可读
PROT_WRITE 可写
flags:(对内存和文件之间关系的描述)
MAP_SHARED 共享的,对内存的修改会影原文件
MAP_PRIVATE 私有的,对内存的修改不会影响原文件
fd:
文件描述符,需要用open函数打开一个文件
offset:
偏移量
返回值:
成功:返回可用的内存首地址
失败:返回信号MAP_FAILED
*/
//释放内存
int munmap(void *addr, size_t length);
/*
参数说明:
addr:需要释放内存的首地址,一般为mmap的返回值
length:释放内存的长度
返回值:
成功:0
失败:-1
*/
创建book.txt文件,并在book.txt中输入xxxxxxxxxxxxx
mmaptest.c文件
int main()
{
//具有读写的的权利
int fd=open("book.txt",O_RDWR);
//映射长度为8,端口权限为可读可写,内存权限为共享,偏移量为0
char* mem=mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//如果没有映射成功
if(mem==MAP_FAILED)
{
perror("mem err");
return -1;
}
strcpy(mem,"hello");
munmap(mem,8);
close(fd);
return 0;
}
编译文件
$ gcc mmaptest.c -o mmaptest
$ ./mmaptest
$ cat book.txt
输出:helloxxxxxxxxxxxxxxxxx
- 如果更改mem变量的地址,释放mummap时,mem传入失败
- 文件的偏移量,应该是是4k(4096)的整数倍。
- open文件选择O_WRONLY可以吗?不可以,内存映射的过程有读取文件的操作
- 选择MAP_SHARED的时候,,prot选择PROT_READ|PROT_WRITE,open文件应该选择可读可写O_RDWR。否则权限会发生冲突。
int main()
{
//创建映射区
int fd=open("book.txt",O_RDWR);
int* mem=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(mem==MAP_FALIED)
{
perror("mem err");
return -1;
}
//创建子进程
pid_t pid=fork();
//修改内存映射区的值
if(pid==0)
{
*mem=100;
printf("child mem: %d\n",*mem);
sleep(3);
printf("child mem: %d\n",*mem);
}
else if(pid>0)
{
sleep(1);
printf("parent mem: %d\n",*mem);
*mem=101;
printf("parent mem: %d\n",*mem);
//阻塞等待杀死子进程
wait(NULL);
}
close(fd);
return 0;
}
在上述父子进程通信中,需要打开一个文件作为通信的中介。对于空间是一种占用,所以linux文件系统有匿名映射的方式。
使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易。但是缺点是,每次创建映射区都依赖一个文件才能完成,通常建立映射区要open一个临时文件,创建好了再unlink,close。
linux系统提供了创建匿名映射区的方法,无需依赖一个文化即可创建映射区。同样需要借助标志位参数flags来指定。使用宏MAP_ANONYMOUS (或者MAP_ANON)
//例子
int*p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS, -1, offset);
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
int fd=open("/dev/zero",O_RDWR);
// /dev/zero可以随意的映射 /dev/null一般错误信息重定向到改文件中
void*p=mmap(NULL,size,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0)
对于上述父子进程通信的程序,只需要修改创建映射区的部分
int* mem=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0)
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。**由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。**只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。
mmapnorelt_w.c用于修改共享内存区域的数据
typedef struct student
{
int number;
char name[20];
}student;
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("./a.out filename\n");
return -1;
}
//打开文件
int fd=open(argv[1],O_RDWR);
ftruncate(fd,sizeof(student));
int length=sizeof(student);
//创建映射区
student* stu=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu==MAP_FAILED)
{
perror("stu err");
return -1;
}
//改变内存数据
int num=1;
while(1)
{
stu->number=num;
sprintf(stu->name,"stu_name%4d",num++);
sleep(1);
}
//关闭内存映射区
munmap(stu,length);
close(fd);
return 0;
}
mmapnorelt_r.c,用于读取内存映射区域的数据
typedef struct student
{
int number;
char name[20];
}student;
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("./a.out filename\n");
return -1;
}
//打开文件
int fd=open(argv[1],O_RDWR);
int length=sizeof(student);
student* stu=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu==MAP_FAILED)
{
perror("stu err");
return -1;
}
while(1)
{
printf("stu->number: %d stu->name: %s\n",stu->number,stu->name);
sleep(1);
}
//关闭内存区域和文件
munmap(stu,length);
close(fd);
return 0;
}
编译文件
$ touch norelt.txt
$ gcc mmapnorelt_r.c -o mmapnorelt_r
$ gcc mmapnorelt_w.c -o mmapnorelt_w
$ ./mmaonorelt_w norelt.txt
$ ./mmaonorelt_r norelt.txt
感谢阅读!