linux系统调用 高级IO

一、本章概述

	· 本章所讲的高级IO有哪些?
(1)非阻塞IO
(2)记录锁(文件锁)
(3)io多路复用(I/O multiplexing)
(4)异步IO
(5)存储映射
	
	本章所有的内容都与文件的IO有关(数据读写),只要涉及到文件的IO操作,就必然有文件描述符这个东西,
所以本章所有的IO高级操作,都是使用fd来实现的。
	
	本章除了第5个“存储映射”外,其它高级IO操作都必须依赖fcntl函数的支持,所以对于本章来说,fcntl函数
很重要。

	我们在讲第一章时就讲过这个函数,这个函数是一个杂物箱,有很多功能,但是第一章后这个函数一直
没有怎么用,所以大家可能觉得有些生疏,但是不要紧,本章将会频繁的使用这个函数,经过了本章的学习后,
大家对fcntl将不会再陌生。

二、非阻塞IO

2.1 如何实现非阻塞读

(1)打开文件时指定O_NONBLOCK状态标志
		fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
   if(fd < 0)
   {    
				perror("open /dev/input/mouse1 is fail");
       exit(-1);
   }   
		
		在讲IPC有名管道时,如果不希望阻塞的话,就可以在open打开“有名管道”时,指定
	O_NONBLOCK,然后读有名管道无数据时就不会阻塞。
		
		
		
(2)通过fcntl函数指定O_NONBLOCK来实现

	什么情况下会使用fcntl来实现,
	
	1)情况1:当文件已经被open打开了,但是open是并没有指定你要的文件状态标志,而你又不下你
	给去修改open的参数,此时就是可以使用fcntl来重设或者补设。
	
	2)情况2:没办法在open指定,你手里只有一个文件描述符fd,此时就使用fcntl来重设或者补设
			比如无名管道,无名管道连名字都没有,没办法使用open函数,无名管道是使用pipe函
			数来返回文件描述符的,如果过你想非阻塞的读无名管道的话,是没有办法通过open来
			指定O_NONBLOCK的,此时就需要使用fcntl来重设或者补设。
	
	
			当然我们使用fcntl不仅仅只能重设或者补设O_NONBLOCK,也可以重设或者补设
		O_TRUNC/O_APPEND等任何你需要的“文件状态”标志。
	
	
		例子:将0设置为O_NONBLOCK。
		
		设置有两种方式:
		· 重设
			fcntl(0, F_SETFL, O_RDONLY|O_NONBLOCK);
		
		· 补设
			flag = fcntl(0, F_GETFL); 	//获取原有文件状态标志
			flag = flag | O_NONBLOCK; 	//通过|操作,在已有的标志上增设O_NONBLOCK
			fcntl(0, F_SETFL, flag); 		//将修改后的“文件状态标志”设置回去

三、文件锁

	文件锁也被称为记录所,文件锁如果深讲的话,内容不少(比如文件锁最起码分为了建议锁和强制
性锁)。
	
	但是我们这里不准备深讲,深讲没有任何意义,因为在后面的嵌入式课程和实际开发中,文件锁用
到的机会并不多,那我们为什么还要讲呢?

	主要是为了对比学习各种的加锁机制,比如进程有进程信号量加锁机制,线程有线程互斥锁、线程
信号量等加锁机制,学习文件锁有助于我们对比理解,对于我们后续理解驱动课程中的内核锁,c++、
java等库所提供的资源保护的锁机制,都是很有意义的。

	当然还有另一个目的,那就是练习fcntl函数的使用,因为文件锁也需要用到fcntl函数。

	由于文件锁的知识点是刚才所讲的这样一种存在,所以对于文件锁内容的学习,理解>实际使用。

3.1 文件锁的作用

	顾名思义,就是用来保护文件数据的。
	
	当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信
号量来互斥实现,除了可以使用进程信号量以外,还可以使用我们本小节要讲的“文件锁”来实现,而且
功能更丰富,使用起来相对还更容易些。

3.2 多进程读写文件

总结起来就是,多进程读写文件时,如果你想进行资源保护的话,完美的资源保护应该满足如下这样的。				
		1)写与写之间互斥
		2)读与写之间互斥
		3)读与读之间共享
	
		
如何实现以上读写要求?
	如果使用信号量来实现保护的话,只能是一律互斥,包括读与读都是互斥的,不能够向上面描述的,
既能互斥又能共享,但是文件锁可以做到。

3.3 文件锁

3.3.1 文件锁的读锁与写锁

对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”,我们这里简称为读锁和写锁。

读锁、写锁之间关系

(1)读锁和读锁共享:可以重复加读锁,别人加了读锁在没有解锁之前,我依然可以加读锁,这就是
共享。	
	
(2)读锁与写锁互斥:别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
		加锁失败后两种处理方式,
			- 阻塞,直到别人解锁然后加锁成功为止
			- 出错返回,不阻塞
		
		
(3)写锁与写锁互斥:别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
		
		加锁失败后两种处理方式,
			- 阻塞,直到别人解锁然后加锁成功为止
			- 出错返回,不阻塞
		
		我们常用的是阻塞加锁,至于如何实现阻塞和非阻塞,后面详细讲。

3.3.2 使用文件锁对文件进行保护

		读文件时加读锁,写文件时就加写锁,然后就可以很容易的实现符合如下要求的资源保护。
		
		1)写与写之间互斥
		2)读与写之间互斥
		3)读与读之间共享

3.4 文件锁的加锁方式

(1)对整个文件内容加锁

	对整个文件加锁是最常用的文件锁的加锁方式。

	当你对整个文件加锁时,如果文件的长度因为写入新数据或者截短而发生了变化,加锁内容的长度会
自动变化,保证对内容变化着的整个文件加锁。
		
		
(2)对文件某部分内容加锁			
				
	不过一般来说是,对多少内容加锁,就对多少内容解锁,如果你是对整个文件加锁,就将整个文件
解锁。
	
	但是实际上加锁和实际解锁的长度可以不相同,比如我对1000个字节的内容加了锁,但是可以只对
其中的100字节解锁,不过这种情况用的少,知道有这么回事即可。
	
怎么实现文件的整个加锁和区域加锁呢?
后面再详细介绍。

3.5 文件锁的实现

	实现文件锁时,我们还是需要使用fcntl函数。

3.5.1 再看看fcntl的函数原型

#include 
#include 

int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );

第三个参数是...,fcntl函数是一个变参函数,第三个参数用不到时就不写。

(1)功能

		fcntl函数有多种功能,我们这里主要介绍实现文件锁的功能,当cmd被设置的是与文件锁相关
	的宏时,fcntl就是用于实现文件锁。

(2)返回值:成功返回0,失败则返回-1,并且errno被设置。		
		
(3)参数
	1)fd:文件描述符,指向需要被加锁的文件。
	2)cmd:实现文件锁时,cmd有三种设置,F_GETLK、F_SETLK和F_SETLKW含义如下:
	 
	 (a)F_GETLK
		从内核获取文件锁的信息,将其保存到第三个参数,此时第三个参数为struct 
	flock *flockptr。我们这里是要设置文件锁,而不是获取已有文件锁的信息,我们这
	里用不到这个宏。
				
	 (b)F_SETLK
		设置第三个参数所代表的文件锁,而且设置的是非阻塞文件锁,也就是如果加锁失败不会阻塞。
		也就是说加锁失败后如果不想阻塞的话,就是由F_SETLK宏来决定的。
		
		此时需要用到第三个参数,struct flock *flockptr。
		
		使用举例:
		· 第一步:定义一个struct flock flockptr结构体变量(这个结构体变量就是文件锁)。
		· 第二步:设置flockptr的成员,表示你想设置什么样的文件锁。
		· 第三步:通过第三个参数,将设置好的flockptr的地址传递给fcntl,设置你要的文件锁
			
			
	 (c)F_SETLKW
		与F_SETLK一样,只不过设置的是阻塞文件锁,也就说加锁不成功的话就阻塞,是由F_SETLKW
	宏来决定的。
			
			
	int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
	3)第三个参数
		第三个参数设置为什么视情况而定,如果fcntl用于实现文件锁的话,第三个参数为
	struct flock *flockptr,flockptr代表的就是文件锁。
		
		对flockptr的成员设置为特定的值,就可以将文件锁设置为你想要的锁。
		
	struct flock结构体如下:
	(a)结构体原型
		struct flock
		{
		short l_type;   // Type of lock: F_RDLCK,F_WRLCK, F_UNLCK 
		short l_whence; //How to interpret l_start:SEEK_SET, SEEK_CUR, SEEK_END
		off_t l_start;   // Starting offset for lock 
		off_t l_len;    //Number of bytes to lock 
		pid_t l_pid;    //PID of process blocking our lock(F_GETLK only) 
		}

		成员说明:
		· l_type:锁类型
			- F_RDLCK:读锁(或称共享锁)
			- F_WRLCK:写锁
			- F_UNLCK:解锁 
				
		· l_whence:加锁位置粗定位,设置同lseek的whence
			- SEEK_SET:文件开始处 
			- SEEK_CUR:文件当前位置处
			- SEEK_END:文件末尾位置处
			
			l_whence这个与lseek函数的whence是一个含义,
				off_t lseek(int fd, off_t offset, int whence);
			
			在第1章文件IO我们就详细的讲过lseek函数。
			
		· l_start:精定位,相对l_whence的偏移,与lseek的offset的含义完全一致
			
			通过l_whence和l_start的值,就可以用来指定从文件的什么位置开始加锁,不过一
		般来说,我们会将l_whence指定为SEEK_SET,l_start指定为0,表示从整个文件头上
		开始加锁。
			
			
		· l_len:从l_whence和l_start所指定的起始地点算起,对文件多长的内容加锁。 
				如果l_len被设置0,表示一直加锁到文件的末尾,如果文件长度是变化的,将自动
			调整加锁的末尾位置。
				
				将l_whence和l_start设置为SEEK_SET和0,然后再将l_len设置为0,就表示从
			文件头加锁到文件末尾,其实就是对整个文件加锁。
				flockptr.l_whence=SEEK_SET;
				flockptr.l_start=0;
				flockptr.l_len=0;
				就就表示对整个文件加锁。
				
				如果只是对文件中间的某段加锁,这只是区域加锁,加区域锁时可以给文件n多个的
			独立区域加锁。
			
			
		· l_pid:当前正加着锁的那个进程的PID	
		
				只有当我们获取一个已存在锁的信息时,才会使用这个成员,这个成员的值不是我们
			设置的,是由文件锁自己设置的,我们只是获取以查看当前那个进程正加着锁。
				
				对于我们目前设置文件锁来说,这个成员用不到。

3.5.2 代码演示

使用文件锁的互斥操作,解决父子进程向同一文件写“hello ”,“world\n”时,hello hello world
相连的问题。

file_lock.h:

#ifndef H_FILELOCK_H
#define H_FILELOCK_H

#include 
#include 

//非阻塞设置写锁
#define SET_WRFLCK(fd, l_whence, l_offset, l_len)\
	set_filelock(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len)
//阻塞设置写锁
#define SET_WRFLCK_W(fd, l_whence, l_offset, l_len)\
	set_filelock(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len)

//非阻塞设置读锁
#define SET_RDFLCK(fd, l_whence, l_offset, l_len)\
	set_filelock(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len)
//阻塞设置读锁
#define SET_RDFLCK_W(fd, l_whence, l_offset, l_len)\
	set_filelock(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len)

//解锁
#define UNLCK(fd, l_whence, l_offset, l_len)\
	set_filelock(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len)

/* 调用这个函数,即可实现阻塞加读锁/阻塞加写锁, 非阻塞加读锁/非阻塞加写锁/解锁 */
static void set_filelock(int fd, int ifwait, int l_type, int l_whence, int l_offset, int l_len)
{
	int ret = 0;	
	struct flock flck;
	
	flck.l_type = l_type;
	flck.l_whence = l_whence;
	flck.l_start = l_offset;
	flck.l_len = l_len;

	ret = fcntl(fd, ifwait, &flck);
	if(ret == -1)
	{
		perror("fcntl fail");
		exit(-1);
	}
} 



#endif


write_hello_world.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "file_lock.h"


void print_err(char *str, int line, int err_no)
{
        printf("%d, %s: %s\n", line, str, strerror(err_no));
        exit(-1);
}

int main(void)
{	
	int fd = 0;
	int ret = 0;
	
	fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
	if(fd == -1) print_err("./file", __LINE__, errno);

	ret = fork();
	if(ret > 0)
	{
		
		while(1)
		{
			SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
			write(fd, "hello ", 6);
			write(fd, "world\n", 6);
			UNLCK(fd, SEEK_SET, 0, 0);
		}
	}	
	else if(ret == 0)
	{	
		while(1)
		{
			SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
			write(fd, "hello ", 6);
			write(fd, "world\n", 6);
			UNLCK(fd, SEEK_SET, 0, 0);
		}
	}	
	
	return 0;
}	
	
	

3.5.3 文件锁的原理

linux系统调用 高级IO_第1张图片

链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了。
	锁节点记录了锁的基本信息。
	· 锁类型
	· 加锁的起始位置(l_whence、l_start)
	· 加锁的长度(l_len)
	· 当前正在加着锁的那个进程的PID
	
	
	加锁时,进程会检查共享的文件锁链表。
	
(1)进程想加读锁
	1)如果链表上只有读锁节点
			所有目前其它进程对该文件只加了读锁,由于读锁时共享的,所以不管
		链表上有几个读锁节点,当前进程都能成功加读锁。
			
		提供:链表上可不可以存在n多个读锁节点?
		答:可以,因为读锁是共享的,不管别的进程有没有解读锁,所有的进程都可以加读锁,
			每加一个读锁,链表上就多一个读锁节点,只有当解锁时节点才被删除。
			
	2)如果链表上有一个写锁节点
			表明目前有进程对文件加了写锁,锁节点还存在,表示人家目前还没有解锁,读锁和写
		锁是互斥的,所以当前不能加读锁,别人解锁后才能加读锁,加锁后链表上就插入一个读锁
		节点。
			
	
		提问:链表上能不能同时存在多个写锁节点
		
		答:不可能,因为写锁是互斥的,目前只能有一个进程在给文件加写锁,在解锁之前,别的
		进程不能加写锁。所以链表上不可能有>一个的写锁节点,否者就不能实现互斥了。
		
		
		提问:链表上会不会同时存在读锁节点和写锁节点?
			读锁节点和写锁节点也是互斥的,链表上有读锁节点就不可能存在写锁节点,反过来有写
		锁节点就不可能有读锁节点。
	
	
(2)你想加写锁
		1)如果链表上有读锁节点,别人还没有解锁,读锁与写锁互斥,不能加写锁。
				
		2)如果链表上有写锁节点,别人还没有解锁,写锁与写锁互斥,多以当前进程不能加写锁


(3)对比进程信号量
		1)进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作
		2)文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作

3.5.4 文件锁其它值得注意的地方

(a)在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符,
		那么该进程加在文件上的文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节
		点会被删除。
		
			进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁。

(b)父进程所加的文件锁,子进程不会继承,我们在讲进程控制时就说过
			加锁是进程各自私人事情,不能继承,就好比你老爸有抽烟的嗜好,难道这也需要继承吗
		,肯定不是的。

3.5.5 提一个问题:多线程间能不能使用fcntl实现的文件锁呢?

	可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述
符才有效。

3.6 使用flock函数来实现文件锁

3.6.1 函数原型

	#include
				
  int flock(int fd, int operation);
		
		1)功能
				按照operation的要求,对fd所指向的文件加对应的文件锁。
				
				加锁不成功时会阻塞。
				
		2)返回值:成功返回0,失败返回-1,errno被设置
		
		3)参数
			(a)fd:指向需要被加锁的文件
			(b)operation
					· LOCK_SH:加共享锁
  			· LOCK_EX:加互斥锁
  			· LOCK_UN:解锁

3.6.2 代码演示

1)用于多进程
		flock用于多进程时,各进程必须独立open打开文件,对于非亲缘进程来说,不用说打开文件
	时肯定是各自独立调用open打开的。
		
		需要你注意的是亲缘进程(父子进程),子进程不能使用从父进程继承而来的文件描述符,
	父子进程flock时必须使用独自open所返回的文件描述符。
		
		这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁
		
	· 共享锁与互斥锁之间互斥
	· 互斥锁与互斥锁之间互斥
	· 共享锁与共享锁之间共享
	
	
	
2)用于多线程
	用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。
	fcntl函数实现的
	
	flock

四、异步io

4.1 异步IO的原理

	异步IO的原理就是,底层把数据准备好后,内核就会给进程发送一个“异步通知的信号”通知进程,表
示数据准备好了,然后调用信号处理函数去读数据,在没有准备好时,进程忙自己的事情。
		
	这就好比我跟澡堂老板说一声“有位置了打电话给我啊”,我就回去该忙啥就忙啥了,等老板通
知我了我就知道有位置了,这样的方式不就更好吗。

	比如使用异步IO读鼠标,底层鼠标驱动把数据准备好后,会发一个“SIGIO”(异步通知信号)给进
程,进程调用捕获函数读鼠标,读鼠标的SIGIO捕获函数需要我们自己定义。

4.2 使用异步IO方式读鼠标和键盘

	进程正常阻塞读键盘,然后将读鼠标设置为异步IO方式。


	进程正常阻塞读键盘时,如果鼠标没有数据的话,进程不关心读鼠标的事情,如果鼠标数据来了,
底层鼠标驱动就会向进程发送一个SIGIO信号,然后调用注册的SIGIO信号捕获函数读鼠标数据。
	
	当然也可以反过来,进程正常阻塞读鼠标,然后将读键盘设置为异步IO方式。
	
	
	异步IO这个名字怎么理解?
		比如以异步IO方式读鼠标数据为例,如果知道什么时间数据会来,等这个时间到时再去读数据
	,这就是步调统一的同步读。
		
		如果不知道什么时候会有数据来,这种就只能是什么时候数据来了就什么时候读,这种就是
	异步的读。之所叫异步,是因为我不知道你什么时候来,没办法统一步调(异步的),只能是
	随时来是随时读。
		
	
	不过使用异步IO有两个前提,
	(1)底层驱动必须要有相应的发送SIGIO信号的代码,只有这样当底层数据准备好后,底层才会发送
			SIGIO信号给进程。
			
			我们之所以可以对鼠标设置异步IO,是因为人家在实现鼠标驱动时,有写发送SIGIO信号
		的代码,如果驱动程序是我们自己写的,发送SIGIO的代码就需要我们自己来写。
		
		
	(2)应用层必须进行相应的异步IO的设置,否者无法使用异步IO
				应用层进行异步IO设置时,使用的也是fcntl函数。

4.3 使用异步IO时,应用层的设置步骤

(1)调用signal函数对SIGIO信号设置捕获函数
			在捕获函数里面实现读操作,比如读鼠标。

(2)使用fcntl函数,将接收SIGIO信号的进程设置为当前进程
		如果不设置的,底层驱动并不知道将SIGIO信号发送给哪一个进程。
			fcntl(mousefd, F_SETOWN, getpid());	

(3)使用fcntl函数,对文件描述符增设O_ASYNC的状态标志,让fd支持异步IO
			mousefd = open("/dev/input/mouse1", O_RDONLY); 
			
			flag = fcntl(mouse_fd, F_GETFL);
			flag |= O_ASYNC;	//补设O_ASYNC
			fcntl(mouse_fd, F_SETFL, flag);

4.4 代码演示

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 



void print_err(char *str, int line, int err_no)
{
        printf("%d, %s: %s\n", line, str, strerror(err_no));
        exit(-1);
}


int mousefd = 0;
void signal_fun(int signo)
{
	int buf;
	int ret = 0;

	if(SIGIO == signo)
	{
		bzero(&buf, sizeof(buf));
		ret = read(mousefd, &buf, sizeof(buf));
		if(ret > 0) printf("%d\n", buf);
	}
}

int main(void)
{
	int ret = 0;
	char buf[100] = {0};
	struct pollfd fds[2];
	
	mousefd = open("/dev/input/mouse0", O_RDONLY);
	if(mousefd == -1) print_err("open mouse0 fail", __LINE__, errno);
		
	//为SIGIO设置捕获函数,在捕获函数里面读鼠标	
	signal(SIGIO, signal_fun);
	
	//告诉鼠标驱动,他发送的SIGIO信号由当前进程接收
	fcntl(mousefd, F_SETOWN, getpid());	

	//对mousefd进行设置,让其支持异步IO
	int flg = fcntl(mousefd, F_GETFL);
	flg |= O_ASYNC;
	fcntl(mousefd, F_SETFL, flg);
	
	
	while(1)
	{
		bzero(buf, sizeof(buf));
		ret = read(0, buf, sizeof(buf));
		if(ret > 0) printf("%s\n", buf);
	}

	return 0;
}


五、存储映射

5.1 普通读写文件方式的缺点

	面对大量数据时显得很吃力,比如应用层有超大量的数据,需要保存到“硬盘”的某个普通文件上。
应用程序向“显示器”这个字符设备文件的“显存”写视频和图片数据。大家都知道视频和图片的数据量非常
大,特别是视频,

5.1.1 普通读写文件的特点

	使用文件IO的read/write来进行文件的普通读写时,函数经过层层的调用后,才能够最终操作到
文件,	比如我们以读(read函数)为例:

		应用层  read(fd, buf, sizeof(buf)); //buf应用缓存
							|
	------------|--------------------------------------
		OS层      |                                 
					 xxx1_read(**, xxx1_buf, **);   //xxx_buf1内核缓存
							|
							|
					 xxx2_read(**, xxx2_buf, **);   //xxx_buf2内核缓存
							|
							|
						......
							|
							|
					xxxn_read(**, xxxn_buf, **);   //xxxn_buf内核缓存
	------------|----------------------------------------
		  		    |
					  文件
	
	以上画的只是一个示意的过程,不是说这些函数就一定叫这样的名字,但是不管怎么说上图能够很好反
映出,在读写时中间会有很多的调用过程,数据需要在不同的缓存间倒腾,最终才能从文件到应用缓存
,或者从应用缓存到文件,效率很低。

	疑问:为什么中间经过一系列的捣腾后效率会很低呢?
	(1)cpu执行一堆的函数,很耗费cpu资源,而且浪费时间
	(2)中间一堆的缓存都是函数从内存开辟的,浪费内存资源,而且数据在各缓存间倒腾时也很耗费
	时间
	
	总之read、write这种普通读写文件的方式效率不高,那效率不高还要它干嘛?
	因为对于数据量较少的情况来说,这种普通读写方式还是非常方便的,而且数据量较少时,效率并不
会太低,只有当数据量非常大时,效率的影响才会非常的明显。
	
	所以对于数据量很少的情况来说,我们最常用的还是普通的读写方式。
	
	
	疑问:使用标准IO函数的读和写,中间过程也很多吗?
	答:当然,因为“标准io”本来就是封装文件IO来实现的。

5.2 存储映射所用的mmap函数

	mmap是memory map的缩写,map就是映射的意思,有些同学可能会说map不是地图的意思吗,map
当动词时也有映射的意思。其实“地图”本身就真实地理环境的映射,所以地图本身就有映射的意思。

5.2.1 mmap的原理

	比如以映射普通文件为例:
			
			
		mmap的原理就是,既然直接read、write很费劲,那我干脆抛弃read、write的操作,mmap
	采用直接映射的方式实现,mmap映射时,比如映射普通文件,其实就会将普通文件的硬盘空间
	的物理地址映射到进程空间的虚拟地址。
		
		
		通常情况下,进程空间的虚拟地址只映射自己底层物理空间的物理地址,但是使用mmap时,
	他会将文件的硬盘空间的地址也映射到虚拟地址空间,这么一来应用程序就可以直接通过映射的
	虚拟地址操作文件,根本就不需要read、write函数了,使用地址操作时省去了繁杂的中间调用过
	程,可以快速对文件进行大量数据的输入输出。
	
	疑问:使用存储映射时,read、write被省掉了,open是不是也被省掉了?
	答:open不能省,必须要将文件open后,才能使用mmap进行映射。

5.2.2 映射时,具体映射到了进程空间的什么位置呢?

	映射到了“进程应用空间”堆和栈中间那片虚拟地址的位置。
	
(1)进程内核空间:用于映射“OS”所在的物理内存空间
(2)进程应用空间:用于映射“应用程序”所在的物理内存空间

5.2.3 对比IPC之共享内存

	大家思考下,本小节讲的存储映射与共享内存是不是很像。

(1)回顾共享内存
			共享内存是让不同的进程空间映射到同一片物理内存上,然后通过共享的物理内存来实现
		进程间通信。
		
		共享内存的使用过程:
		
		1)shmget:创建共享内存,其实就是从物理内存中划出一片准备用于共享的空间出来
		
		2)shmat:调用该函数的进程,将自己的进程空间的某片虚拟地址映射到共享内存上
				程序员不需要知道共享内存空间的起始物理地址是多少,shmat它会知道。
		
			至于说映射后的起始虚拟地址一般也是由shmat自己选择的,程序员不需要干预。
						
		3)shmdt:取消映射
		
		4)shmctl:将被取消映射的共享内存删除(释放)
			
			
			
(2)对比存储映射和共享内存

		1)存储映射,其实也可以用来实现进程间通信
				
			比如A和B进程都映射到同一个普通文件上,这时A进程往里写数据,B进程从里面读
		数据,反过来也是一样的,如此就实现了进程间的通信。
			
			但是这顶多只算是广义上的通信,所谓广义上的通信就是,只要不是OS提供专门的
		IPC,就不是专门的进程间通信,只能算是广义的IPC。
			
			实际上,我们也不会使用mmap映射普通文件来实现进程间通信,因为操作硬盘的速度相
		比操作内存来说低了很多,如果你想实现进程间大量数据通信的话,完全可以使用与存储映
		射原理类似的“共享内存”来实现,而且速度很快。
			
			
		2)虽然存储映射和共享内存原理相似,但是各自用途不同
			
			(a)共享内存
					实现进程间大量数据通信(共享)。
						
				
			(b)存储映射
					对文件进行大量数据的高效输入输出。

5.2.4 mmap函数

(1)函数原型
#include 

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


(2)功能:将文件所在的磁盘空间映射到进程空间。
	
(3)返回值:调用成功,返回映射的起始虚拟地址,失败则返回(void*)-1,errno被设置。
		
		
		
(4)参数
	1)addr:人为指定映射的起始虚拟地址
		如果设置为NULL,表示由内核决定映射的起始虚拟地址,这也是最常见的设置方式,这与我们
	调用shmat映射共享内存时指定NULL是一样的。
		
		如果设置不为NULL,就表示由自己指定,指定的起始虚拟地址必须是虚拟页(4k)的整数倍,
	这与自己指定shmat的映射起始虚拟地址也是一样的。

		
	2)length:映射长度,也就是你想对文件映射多长。
	
	3)prot:指定对映射区的操作权限,可指定如下宏:
	 (a)PROT_EXEC:映射区的内容可执行。
			如果你映射的是普通文件是一个可执行文件的话,将映射权限指定为PROT_EXEC后,
		是能够通过映射后的虚拟地址去执行文件中的“指令”。
	 
	 (b)PROT_READ:映射区的内容可读。
	 
	 (c)PROT_WRITE:映射区的内容可写。
			以上三种选项可相互 | 操作。
			比如:PROT_EXEC | PROT_READ
			
	 (d)PROT_NONE:映射区不允许访问(不允许执行、读、写),一般不会指定这个,如果指定为不可
				访问的话,映射就没有意义了。

		 
	4)flags:向映射区写入了数据,是否将数据立即更新到文件中。
		 (a)MAP_SHARED:立即更新。
		
		
	5)fd:需要被映射文件的描述符。
	
	6)offset:
			表示从文件头的offset处开始映射。
	
			一般都指定为0,表示从文件头开始映射。

7.2.5 munmap

		(1)函数原型
				#include 
				
				int munmap(void *addr, size_t length);
				
		(2)功能:取消映射
		
		(3)返回值:调用成功返回0, 失败则-1, errno被设置。
		
		(4)参数
				1)addr:映射的起始虚拟地址
				2)length:需要取消的长度

7.2.6 代码演示

	写一个例子程序,将A文件的大量数据复制到B文件中。
	
	如果采用传统方式,使用read函数从A文件读出数据,然后向B文件write,如果数据量很大的话
,复制的效率会非常低,此时我们就可以使用存储映射来实现。

	mmap映射文件size为0的文件时,会映射失败,映射失败时内核会向进程发送一个SIGBUS信号,
提示mmap失败了,这个信号的默认处理方式是终止,所以当进程收到这个信号时就被异常终止了。

	如果你不想被这个信号终止,你可以自己忽略或者屏蔽这个信号,一般来说我们不需要忽略和屏蔽
该信号。

代码演示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 



void print_err(char *str, int line, int err_no)
{
        printf("%d, %s: %s\n", line, str, strerror(err_no));
        exit(-1);
}

int main(void)
{
	int srcfd = -1;
	int dstfd = -1;
	void *srcaddr = NULL;
	void *dstaddr = NULL;

	
	/* 打开源文件 */
	srcfd = open("./file_lock.h", O_RDWR);
	if(srcfd == -1) print_err("open file_lock.h fial", __LINE__, errno); 	
	
	/* 打开目标文件 */
	dstfd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
	if(dstfd == -1) print_err("open file fial", __LINE__, errno); 	
	
	/* mmap映射源文件 */	
	struct stat src_stat = {0};
	fstat(srcfd, &src_stat);//获取文件属性(主要是想得到文件的size)
	srcaddr = mmap(NULL, src_stat.st_size, PROT_READ, MAP_SHARED, srcfd, 0);
	if(srcaddr == (void *)-1) print_err("mmap srcfile fail", __LINE__, errno);
	
	/* 将目标文件的长度截断至跟源文件大小一致,防止报错 */
	ftruncate(dstfd, src_stat.st_size);
	/* mmap映射目标文件 */	
	/*
	参数1:指定映射的起始虚拟地址,如果制定NULL表示由mmap指定
	参数2: 要映射的长度
	参数3:指定映射后的操作权限,PROT_WRITE/PROT_READ/PROT_EXEC/PROT_NONE
	参数4:是否立即更新到文件中,指定MAP_SHARED,表示理解更新
	参数5:你要映射的那个文件的fd
	参数6:指定一个偏移,表示你要从文件的什么位置开始映射 */
	dstaddr = mmap(NULL, src_stat.st_size, PROT_WRITE, MAP_SHARED, dstfd, 0);
	if(dstaddr == (void *)-1) print_err("mmap dstfile fail", __LINE__, errno);	
	
	
	/* 想办法讲源文件的数据复制到目标文件中 */
	memcpy(dstaddr, srcaddr, src_stat.st_size);

	
	return 0;	
}	
	

你可能感兴趣的:(linux系统编程,linux,服务器)