138 Linux 系统编程15 进程间通讯IPC ,:管道pipe函数用于血缘关系的进程间通讯,FIFO有名管道,使用文件完成进程间通讯,共享存储映射使用mmap 函数

一 进程间通讯,IPC

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

IPC 原理是因为:在MMU 管理 虚拟内存和物理内存的时候,实际上所有程序的kernel都会被映射到同一块 物理内存,因此才可以在这个同一块的 物理 内存中,建立一块缓冲区,然后让a 和 b 通讯

二 如何完成进程间通讯

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

① 管道 (使用最简单)

② 信号 (开销最小)

③ 共享映射区 (无血缘关系)

④ 本地套接字 (最稳定)

三 管道的概念:用于血缘关系的进程之间

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。

调用pipe系统函数即可创建一个管道。有如下特质:

1. 其本质是一个伪文件(实为内核缓冲区)

2. 由两个文件描述符引用,一个表示读端,一个表示写端。

3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

管道的局限性:

① 数据不能进程自己写,自己读。

② 管道中数据不可反复读取。一旦读走,管道中不再存在。 

③ 采用半双工通信方式,数据只能在单方向上流动。

④ 只能在有公共祖先的进程间使用管道。

常见的通信方式有,单工通信、半双工通信、全双工通信。

四 pipe函数:创建并打开管道 重点
 

原理:

创建管道,如上图 画的buf,实际上pipe就是创建这两个buf

    int pipe(int pipefd[2]); 成功:0;失败:-1,设置errno

函数调用成功返回r/w两个文件描述符。无需open,但需手动close。

规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。

向管道文件读写数据其实是在读写内核缓冲区。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:

自己的理解如下:

1. 当fork之后,由于父子进程共享了文件描述符,理论上如下图的

2.那么这样我们就知道怎么弄了,(将父进程的读端关闭 && 将子进程的写端关闭)或者  (将父进程的写端关闭 && 将子进程的读端关闭)

代码实现

//管道pipe,我们这个程序中规定,从子进程写数据,从父进程中读数据,以实现父子间通讯
int main() {
	int ret = 0;
	int pipefd[2] = { 0 };
	ret =  pipe(pipefd);
	if (ret == -1) {
		perror("pipe error ");
		return ret;
	}
	pid_t pid = fork();
	if (pid==-1) {
		perror("fork error ");
		return ret;
	}
	if (pid>0) {
		//父进程

		close(pipefd[1]);//关闭父进程写端
		char readdata[1024] = { 0 };
		int realdata = read(pipefd[0],readdata,sizeof(readdata));
		cout << realdata << "   " << readdata << endl;
	}
	if (pid==0) {
		//子进程
		sleep(2);
		close(pipefd[0]);//关闭子进程读端
		char * data = "nihaochina";
		write(pipefd[1],data,strlen(data));
		close(pipefd[1]);
	}

	return ret;
}

管道的读写行为:

① 读管道:

1. 管道中有数据,read返回实际读到的字节数。

2. 管道中无数据:

                (1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)

                (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

② 写管道:

1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)

2. 管道读端没有全部关闭:

        (1) 管道已满,write阻塞。

        (2) 管道未满,write将数据写入,并返回实际写入的字节数。

练习1 使用管道实现父子进程间通信,完成:ls | wc –l。

假定父进程实现ls,子进程实现wc。

分析:

1. 父进程实现ls,子进程实现wc,两个进程,肯定要用到fork

2.在fork之后,父进程 使用 execlp 实现 ls, 子进程使用 execlp 实现 wc -l

3.由于父进程 ls 的结果,最终给 子进程使用,因此要实现进程间的通讯。因此要使用pipe,且pipe要优先于 fork使用 

4.由于ls命令的结果是输出到屏幕上的,也就是输出到fd为 STDOUT_FILENO指向的屏幕上,现在要从屏幕上,转到 写端口上, 因此要使用重定向技术;同理,wc -l 原本是要从STDIN_FILENO上指向的输入设备上读取数据,现在要从 读端口上, 因此要用到 重定向技术,dup2

测试,

//使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc。
//
//分析:
//
//1. 父进程实现ls,子进程实现wc,两个进程,肯定要用到fork
//
//2.在fork之后,父进程 使用 execlp 实现 ls, 子进程使用 execlp 实现 wc - l
//
//3.由于父进程 ls 的结果,最终给 子进程使用,因此要实现进程间的通讯。因此要使用pipe,且pipe要优先于 fork使用 
//
//4.由于ls命令的结果是输出到屏幕上的,也就是输出到fd为 STDOUT_FILENO指向的屏幕上,
//现在要从屏幕上,转到 写端口上, 因此要使用重定向技术;
//同理,wc - l 原本是要从STDIN_FILENO上指向的输入设备上读取数据,
//现在要从 读端口上, 因此要用到 重定向技术,dup2

int main() {
	int ret = 0;
	int fds[2] = { 0 };
	//1.使用pipe创建两个管道,一个管道是读端,一个管道是写端
	ret = pipe(fds);
	if (ret<0) {
		perror("pipe error");
		exit(ret);
	}

	//2 .使用 fork create 子进程
	pid_t pid = fork();

	if (pid < 0) {
		perror("fork error");
		ret = pid;
		exit(ret);
	}
	if (pid > 0) {
		//父线程
		close(fds[0]);
		ret = dup2(fds[1], STDOUT_FILENO);
		if (ret < 0) {
			perror("dup2 fds[1] error");
			exit(ret);
		}
		//sleep(1);//这里故意让父进程sleep 一秒,观察现象
		execlp("ls","ls",NULL);
		perror("execlp ls error");
		exit(ret);
	}
	else if (pid == 0) {
		//子线程
		close(fds[1]);
		ret = dup2(fds[0],STDIN_FILENO);
		if (ret < 0) {
			perror("dup2 fds[0] error");
			exit(ret);
		}
		//sleep(1);//这里故意让子进程sleep 一秒,将父进程的sleep 1秒关闭,观察现象
		execlp("wc", "wc", "-l", NULL);
		perror("execlp wc -l error");
		exit(ret);
	}

	return ret;
}

练习2:使用管道实现兄弟进程间通信。 兄:ls  弟: wc - l  父:等待回收子进程。

//练习:使用管道实现兄弟进程间通信。 兄:ls  弟: wc - l  父:等待回收子进程。
//要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。

int main() {
	int ret = 0;
	int fds[2] = { 0 };
	//1.使用pipe创建两个管道,一个管道是读端,一个管道是写端
	ret = pipe(fds);
	if (ret < 0) {
		perror("pipe error");
		exit(ret);
	}

	//2 .使用 fork create 两个子进程
	int i = 0;
	for (; i < 2;i++) {
		pid_t pid = fork();

		if (pid < 0) {
			perror("fork error");
			ret = pid;
			exit(ret);
		}
		if (pid == 0) {
			//子进程,要直接break;
			break;
		}
	}

	if (i == 2) {
		//父线程
		close(fds[0]);
		close(fds[1]);
		int status = 0;
		pid_t wpid = 0;
		//非 阻塞 回收 所有的子进程, 注意,循环回收,且判断标准是wpid!=-1,这是因为一旦没有子进程后可以回收,也会返回-1,因此
		//-1 表示error ,errno被设置
		//如果成功,则返回 child pid,是一个大于0的值
		//如果是0,表示不阻塞,且这时候子进程并没有执行完成,无法回收。
		//通过下面的 注释掉的while(true)循环测试,发现,当正确回收两个之后,就一直是-1了,原因是“No child processes”,也就是说,当子进程全部回收完毕的时候,会返回-1,因此我们可以在 回收 子进程的过程中,可以通过值是否是-1,看是否回收完毕、
		//那么这里还有一个问题,就是返回-1的时候就只是没有 没有子进程这一种case 吗?是否在其他情况下也返回-1呢?这里就有一个坑点了,在网上查了查,没有发现其他的说法,但是这里是一个点,后续在遇到复杂的case下,要打印这个失败时候的perror,注意观察提示信息。

		//aaa = 8767
		//	waitpid error : Success
		//	aaa = 8768
		//	waitpid error : Success
		//	aaa = -1
		//	waitpid error : No child processes
		//	aaa = -1
		//	waitpid error : No child processes
		//	aaa = -1
		//	waitpid error : No child processes
		cout << "tttt" << endl;
		while (true) {
			//cout << "vvvvvv" << endl;
			pid_t aaa = waitpid(-1, &status, 0);
			cout << "aaa = " << aaa << endl;
			if (aaa = -1) {
				perror("waitpid error ");
			}
			sleep(1);
		}
		while ((wpid = (waitpid(-1, &status, 0)) != -1)) {
			if (WIFEXITED(status)) {
				cout<<"子进程正常执行完毕,主进程回收 wpid = : "<< wpid << " the  exit  status  of  the child = " << WEXITSTATUS(status);
			}
			if (WIFSIGNALED(status)) {
				cout << "子进程非正常指向完毕,主进程回收 wpid = : " << wpid << " number of the signal that caused the child process to terminate   = " << WTERMSIG(status);
			}
		}
	}
	else if (i == 0) {
		//兄 子进程
		close(fds[0]);
		ret = dup2(fds[1], STDOUT_FILENO);
		if (ret < 0) {
			perror("dup2 fds[1] error");
			exit(ret);
		}
		execlp("ls", "ls", NULL);
		perror("execlp ls error");
		exit(ret);
	}

	else if (i == 1) {
		//弟 子进程
		close(fds[1]);
		ret = dup2(fds[0], STDIN_FILENO);
		if (ret < 0) {
			perror("dup2 fds[0] error");
			exit(ret);
		}
		//sleep(1);
		execlp("wc", "wc", "-l", NULL);
		perror("execlp wc -l error");
		exit(ret);
	}


	return ret;
}

测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?  允许,但是不常用,也不建议用。

注意的是:如果在父进程 有wait函数,且父进程也参与了同时写,或者同时读,可能导致程序卡住,分析这个卡住有可能是因为死锁,但是目前看不到源码,不确定是否是死锁。

因此,即使我们在实际中使用到一个写端,多个读端的case,也让兄弟进程去做这件事,不要让父进程参与。

//测试:是否允许,一个pipe有一个写端,多个读端呢?
//是否允许有一个读端多个写端呢?
int main() {
	//多个读端的情况

	int ret = 0;
	int fds[2] = { 0 };
	ret = pipe(fds);
	if (ret < 0) {
		perror("pipe error");
		exit(ret);
	}
	pid_t pid = fork();
	if (pid < 0) {
		perror("fork error");
		ret = pid;
		exit(ret);
	}
	if (pid > 0) {
		//父线程不关闭读端,只是让子进程关闭写端,这时候,就有两个读端,一个写端在父进程
		//close(fds[0]);
		char *buf = "nihao";
		int writelen = 0;
		cout << "sizeof(buf) = " << sizeof(buf) << endl;
		cout << "strlen(buf) = " << strlen(buf) << endl;
		char readbuf[1024] = {0};
		int realreadlen = 0;
		writelen = write(fds[1],buf,strlen(buf));
		if (writelen < 0) {
			perror("father write fds[1] error");
			ret = writelen;
			exit(ret);
		}
		//sleep(1);//这里故意让父进程sleep 一秒,观察现象
		realreadlen = read(fds[0], readbuf, sizeof(readbuf));
		if (realreadlen < 0 ) {
			cout << "父进程读取数据出错" << endl;
			perror("父进程读取数据出错");
			ret = realreadlen;
			return ret;
		}
		cout << "父进程 realreadlen = "  << realreadlen<< "  readbuf = " << readbuf <<  endl;

		//wait(NULL);//wait函数表示回收任意子进程,传递NULL,表示不关心子进程是如何结束的。
		//这里写wait会有问题,且有两种现象。
		// 现象一:从现象来看子进程在打印了aa1后,就走不动,被卡死,看样子,应该是 卡在 read(fds[0], readbuf, sizeof(readbuf));
		// 现象二:父进程在走到read后卡死
		//说明,在父进程既read又wait,在子进程read的case,如果read 的是同一个fd,则可能有问题。

	}
	else if (pid == 0) {
		//子线程
		cout << "aa" << endl;

		close(fds[1]);

		cout << "aa1" << endl;
		char readbuf[1024] = { 0 };
		int realreadlen = 0;
		realreadlen = read(fds[0], readbuf, sizeof(readbuf));
		cout << "bb" << endl;
		if (realreadlen < 0) {
			cout << "子进程读取数据出错" << endl;
			perror("子进程读取数据出错");
			ret = realreadlen;
			return ret;
		}
		cout << "cc" << endl;
		if (realreadlen == 0 ) {
			//说明已经读到了末尾。
			cout << "子进程没有读取到数据"<<endl;
		}
		cout << "dd" << endl;
		cout << "子进程 realreadlen = " << realreadlen << "  readbuf = " << readbuf << endl;
	}

	return ret;
}

五 管道缓冲区大小 -4096字节

可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:512x8 = 4096字节

pipe size            (512 bytes, -p) 8

也可以使用fpathconf函数,借助参数 选项来查看。使用该宏应引入头文件<unistd.h>

long fpathconf(int fd, int name); 成功:返回管道的大小 失败:-1,设置errno

六 FIFO 非重点,知道怎么写就OK,目前是过时的技术,但是要看懂

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。

FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。

创建方式:

1. 命令:mkfifo 管道名

2. 库函数:int mkfifo(const char *pathname,  mode_t mode);  成功:0; 失败:-1

一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。

//使用 mkfifo 函数创建 有名管道,完成非血缘间的进程间通讯
int main() {
	int ret = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/fifofile";
	ret = access(filename, F_OK);
	if (ret == 0) {
		//file access
		cout << "fifofile存在" << endl;
	}
	else {
		cout << "fifofile不存在,创建fifofile" << endl;
		ret = mkfifo(filename, 0664);
		if (ret == -1) {
			perror("mkfifo error /home/hunandede/projects/linuxcpp/fifofile");
			return ret;
		}
	}
	
	//在这里已经创建了fifofile了
	//那么创建这个fifo,就可以使用open打开它,常见的文件I / O函数都可用于fifo。如:close、read、write、unlink等。
	int fd = 0;
	fd = open(filename, O_RDWR);
	if (fd == -1) {
		perror("open error /home/hunandede/projects/linuxcpp/fifofile");
		fd = ret;
		return ret;
	}

	//我们在这个中主要是给fifofile中写数据
	char *buf = "nihao";
	char writebuf[64] = { 0 };
	int i = 1;
	while (1) {
		sprintf(writebuf, "%s%d", buf, ++i);
		write(fd, writebuf, strlen(writebuf));
		sleep(1);
	}


	close(fd);

	return ret;
}

//使用 mkfifo 函数创建 有名管道,完成非血缘间的进程间通讯
int main() {
	int ret = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/fifofile";
	ret = access(filename, F_OK);
	if (ret == 0) {
		//file access
		cout << "fifofile存在" << endl;
	}
	else {
		cout << "fifofile不存在,创建fifofile" << endl;
		ret = mkfifo(filename, 0664);
		if (ret == -1) {
			perror("mkfifo error /home/hunandede/projects/linuxcpp/fifofile");
			return ret;
		}
	}



	//在这里已经创建了fifofile了
	//那么创建这个fifo,就可以使用open打开它,常见的文件I / O函数都可用于fifo。如:close、read、write、unlink等。
	int fd = 0;
	fd = open(filename, O_RDWR);
	if (fd == -1) {
		perror("open error /home/hunandede/projects/linuxcpp/fifofile");
		fd = ret;
		return ret;
	}

	//我们在这个中主要是给fifofile中写数据

	char readbuf[64] = { 0 };
	int realreadlen = 0;
	while (1) {
		realreadlen = read(fd,readbuf,sizeof(readbuf));
		if (realreadlen == -1) {
			perror("read fd error");
		}
		else if (realreadlen == 0) {
			cout << "读取完成,目前没有数据 = "  << realreadlen <<  endl;
		}
		else if (realreadlen > 0) {
			cout << "realreadlen = " << realreadlen << endl;
			cout << "readbuf = " << readbuf << endl;
		}
		sleep(1);
	}


	close(fd);

	return ret;
}

七 使用文件完成 进程间的通讯,非重点

//使用文件,完成非血缘间的进程间通讯
//文件进程间通信
//使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符。也就共享打开的文件。
//练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。 			   【fork_shared_fd.c】
//思考,无血缘关系的进程可以打开同一个文件进行通信吗?为什么?可以,
int main() {

	int ret = 0;
	int fd = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/createfile.txt";
	ret = access(filename, F_OK);
	if (ret == 0) {
		//file access
		cout << "createfile.txt存在" << endl;
	}
	else {
		cout << "createfile.txt不存在,创建fifofile" << endl;
		fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644);

		if (fd == -1) {
			perror("open file error /home/hunandede/projects/linuxcpp/createfile.txt");
			ret = fd;
			return ret;
		}
	}
	//我们在这个中主要是给fifofile中写数据
	char *buf = "nihao";
	char writebuf[64] = { 0 };
	int i = 1;
	while (1) {
		sprintf(writebuf, "%s%d", buf, ++i);
		write(fd, writebuf, strlen(writebuf));
		sleep(1);
	}

	close(fd);
	

	return ret;
}

int main() {
	int ret = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/createfile.txt";
	ret = access(filename, F_OK);
	if (ret == 0) {
		//file access
		cout << "createfile.txt 存在" << endl;
	}
	else {
		cout << "createfile.txt 不存在,无法读取文件" << endl;
		return ret;
	}



	//在这里已经创建了fifofile了
	//那么创建这个fifo,就可以使用open打开它,常见的文件I / O函数都可用于fifo。如:close、read、write、unlink等。
	int fd = 0;
	fd = open(filename, O_RDWR);
	if (fd == -1) {
		perror("open error /home/hunandede/projects/linuxcpp/createfile.txt");
		fd = ret;
		return ret;
	}

	//我们在这个中主要是给fifofile中写数据

	char readbuf[1024] = { 0 };
	int realreadlen = 0;
	while (1) {
		memset(readbuf,0,sizeof(readbuf));
		realreadlen = read(fd, readbuf, sizeof(readbuf));
		if (realreadlen == -1) {
			perror("read fd error");
		}
		else if (realreadlen == 0) {
			cout << "读取完成,目前没有数据 = " << realreadlen << endl;
		}
		else if (realreadlen > 0) {
			cout << "realreadlen = " << realreadlen << endl;
			cout << "readbuf = " << readbuf << endl;
		}
		sleep(1);
	}


	close(fd);

	return ret;
}

八 使用共享存储映射 重点,效率高

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。

于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

其核心是 CPU中的 MMU 将内存 和 磁盘中的一块文件 映射起来。

使用mmap 函数实现

8.1 mmap 函数

void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);

返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏

参数:

addr: 映射区的首地址,建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL

length:映射区的大小, 欲创建映射区的大小,这个值一般都和物理文件的大小一致,是可以小于真正的物理文件大小的,因为后面有offset,也就是说,映射的内存区是可以映射一部分物理文件的。但是映射区的大小不能大于文件的大小,如果大于文件的大小,那么映射区多出来的那部分内存如果操作了,C++编译器就不知道怎么办了

prot:映射区的权限 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE

flags: 映射区写入磁盘文件的权限 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)

   MAP_SHARED:  会将映射区所做的操作反映到物理设备(磁盘)上。

   MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。

fd: 物理文件的fd 用来建立映射区的文件描述符

offset:物理文件的偏移,从物理文件的哪里开始映射 映射文件的偏移(4k的整数倍),以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射。


[注]mmap使用细节
1.mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。
2. 内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。具体情形参见“情形三”。
3. 映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。

8.2 一般mmap写法

void *mmap(NULL, size_t length, int prot, int flags, int fd, off_t offset);

studentmmap = (STU *)mmap(NULL, sizeof(STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

8.3 注意点:

  1. 创建映射区的过程中,隐含着一次对映射文件的读操作。
  2. 当MAP_SHARED时,要求:映射区的权限应 <= 文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
  3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
  4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。如,400字节大小的文件,在建立映射区时offset 4096字节,则会报出总线错。
  5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
  6. 如果文件偏移量必须为4K的整数倍

     7.mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

8.4 mmap父子进程通信

父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:

MAP_PRIVATE:  (私有映射)  父子进程各自独占映射区;

MAP_SHARED:  (共享映射)  父子进程共享映射区;

//父子
int main() {
	typedef struct student {
		int age;
		char name[128] = { 0 };
	}STU;
	int ret = 0;
	int fd = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/mmapfile.txt";

	fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644);

	if (fd == -1) {
		perror("open file error /home/hunandede/projects/linuxcpp/mmapfile.txt");
		ret = fd;
		return ret;
	}
	cout << "sizeof(STU) = " << sizeof(STU) << endl;
	ret = ftruncate(fd, sizeof(STU));

	//上述是创建了磁盘文件,下面是 将fd映射成为 内存
	STU *studentmmap = NULL;
	studentmmap = (STU *)mmap(NULL, sizeof(STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (MAP_FAILED == studentmmap) {
		perror("mmap error");
		ret = errno;
		return ret;
	}
	struct student stu;
	stu.age = 20;
	strcpy(stu.name, "nihao");

	pid_t pid;
	pid = fork();
	if (pid<0) {
		perror("fork error");
		munmap(filename,sizeof(stu));
		close(fd);
		unlink(filename);
		exit(1);
	}
	else if (pid > 0 ) {

		int i = 10;
		while (i > 0) {
			memcpy(studentmmap, &stu, sizeof(stu));
			stu.age = stu.age + 1;
			//cout << "sizeof(stu) = " << sizeof(stu) << endl;
			i--;
			sleep(2);
		}

		pid_t wpit = wait(NULL);
		cout << "wpit = " << wpit << endl;
		ret = munmap(studentmmap, sizeof(stu));
		cout << "munmap ret = " << ret << endl;
		ret = close(fd);
		cout << "close ret = " << ret << endl;
		ret = unlink(filename);
		cout << "unlink ret = " << ret << endl;
	}
	else if (pid == 0 ) {
		int i = 25;
		struct student sturead;
		while (i > 0) {
			memcpy(&sturead, studentmmap, sizeof(sturead));
			//cout << "sizeof(stu) = " << sizeof(sturead) << endl;
			cout << "sturead.age = " << sturead.age << "  sturead.name = " << sturead.name << endl;
			i--;
			sleep(1);
		}
	}

	return ret;
}

8.5 mmap 两个没有关系的 进程的 例子

mmap无血缘关系进程间通信

实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

值得注意的是:MAP_ANON和 /dev/zero 都不能应用于非血缘关系进程间通信。只能用于亲子进程间。

//使用mmap 函数 将内存和磁盘文件,映射成内存,操作内存 就是操作 磁盘文件
//我们的目的是将一个 struct student 存储起来 
int main() {
	typedef struct student {
		int age;
		char name[128] = { 0 };
	}STU;
	int ret = 0;
	int fd = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/mmapfile.txt";

	fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644);

	if (fd == -1) {
		perror("open file error /home/hunandede/projects/linuxcpp/mmapfile.txt");
		ret = fd;
		return ret;
	}
	cout << "sizeof(STU) = " << sizeof(STU) << endl;
	ret = ftruncate(fd, sizeof(STU));

	//上述是创建了磁盘文件,下面是 将fd映射成为 内存
	STU *studentmmap = NULL;
	studentmmap = (STU *)mmap(NULL, sizeof(STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (MAP_FAILED ==studentmmap) {
		perror("mmap error");
		ret = errno;
		return ret;
	}
	struct student stu;
	stu.age = 20;
	strcpy(stu.name,"nihao");

	int i = 10;
	while (i>0) {
		memcpy(studentmmap, &stu, sizeof(stu));
		stu.age = stu.age + 1;
		cout << "sizeof(stu) = " << sizeof(stu) << endl;
		i--;
		sleep(1);
	}
	
	
	ret = munmap(studentmmap,sizeof(stu));
	cout << "munmap ret = " <<ret <<  endl;
	ret = close(fd);
	cout << "munmap ret = " << ret << endl;
	ret = unlink(filename);
	cout << "munmap ret = " << ret << endl;


	return ret;
}

int main() {
	typedef struct student {
		int age;
		char name[128] = { 0 };
	}STU;
	int ret = 0;
	int fd = 0;
	char *filename = "/home/hunandede/projects/linuxcpp/mmapfile.txt";

	fd = open(filename, O_RDWR );

	if (fd == -1) {
		perror("open file error /home/hunandede/projects/linuxcpp/mmapfile.txt");
		ret = fd;
		return ret;
	}


	//上述是创建了磁盘文件,下面是 将fd映射成为 内存
	STU *studentmmap = NULL;
	studentmmap = (STU *)mmap(NULL, sizeof(STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (MAP_FAILED == studentmmap) {
		perror("mmap error");
		ret = errno;
		return ret;
	}
	//这时候,studentmmap指针中理论上是有值的
	struct student stu;

	int i = 15;
	while (i>0) {
		memcpy(&stu, studentmmap, sizeof(stu));
		cout << "sizeof(stu) = " << sizeof(stu) << endl;
		cout << "stu.age = " << stu.age << "  stu.name = " << stu.name << endl;
		i--;
		sleep(1);
	}
	

	ret = munmap(studentmmap, sizeof(stu));
	cout << "munmap ret = " << ret << endl;
	ret = close(fd);
	cout << "munmap ret = " << ret << endl;
	ret = unlink(filename);//这里实际上没有必要删除文件
	cout << "munmap ret = " << ret << endl;
	if (ret == -1) {
		perror("unlink error");
	}
	return ret;
}

8.6 匿名映射

可以完成有血缘间的通讯,不可用于 无血缘间的通讯。

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。

使用MAP_ANONYMOUS (或MAP_ANON), 如:

int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

    "4"随意举例,该位置表大小,可依实际需要填写。

    【fork_map_anon_linux.c】

需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。

① fd = open("/dev/zero", O_RDWR);

② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值