`

Linux系统编程学习笔记(三)高级文件I/O

阅读更多
高级文件I/O
1、Linux提供的高级I/O操作:
1)Scatter/gatter I/O:可以通过单个系统调用从多个buffer写到文件或者读到多个buffer中。
2)Epoll:是poll()和select()的改进版。
3)Memory-mapped I/O:将一个文件映像到内存,通过内存的操作来操作文件,操作更高效简单。
4)File advice:允许进程向内核提供使用的场景来优化I/O效率。
4)异步I/O:允许进程发出I/O请求后不等待操作完成返回。
2、Scatter/gatter I/O:
通过单个系统调用从单个流读取到或写入多个buffer数据。
优点:对于有些操作更自然、更高效、具有原子性。
1)readv:
从文件描述符fd中读去count个段到多个buffers iov中:
#include <sys/uio.h>

ssize_t readv(int fd,const struct iovec *iov, int count);

2)writev:
从多个buffers中写入入最多count个段到由fd描述符标示的文件中。
#include <sys/uio.h>

ssize_t writev(int fd,const struct iovec *iov, int count);

readv、writev与read、write的行为一直,除了读到多个buffers中或者从多个buffers写
iovec结构:
#include <sys/uio.h>

struct iovec{
	void *iov_base; /* pointer to the start of buffer */
	size_t iov_len; /* size of buffer in bytes */
}

按照iovec[0],iovec[1],...,iovec[count-1]的次序依次读写。
返回值:成功,返回读写的字节数,这个字节数是所有iovec结构中iov_len的总和。
失败返回-1,并设置好errno。
iov_len的总和不能超过SSIZE_MAX,count需要大于0,小于IOV_MAX(<limits.h>中),在linux为1024,
否则返回-1,errno设置为EINVAL,
例子:writev:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/uio.h>

int main(){
	struct iovec iov[3];
	ssize_t nr;
	int fd,i;
	
	char *buf[] = {
		"The term buccaneer comes from the word boucan.\n",
		"A boucan is a wooden frame used for cooking meat.\n",
		"Buccaneer is the West Indies name for a pirate.\n" 
	};
	
	fd = open("buccaneer.txt",O_WRONLY | O_CREAT | O_TRUNC);
	if( fd == -1 ){
		perror("open");
		return 1;
	}

	for(i = 0; i < 3; i++){
		iov[i].iov_base = buf[i];
		iov[i].iov_len = strlen(buf[i]);
	}
	
	nr = writev(fd,iov,3);
	if(nr == -1){
		perror("writev");
		return 1;
	}
	printf("wrote %d bytes\n",nr);

	if(close(fd)){
		perror("close");
		return 1;
	}
	return 0;
}

readv例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>

int main(){
	char foo[48],bar[51],baz[49];
	struct iovec iov[3];
	ssize_t nr;
	int fd,i;

	fd = open("buccaneer.txt",O_RDONLY);
	if(fd == -1){
		perror("open");
		return 1;
	}

	iov[0].iov_base = foo;
	iov[0].iov_len = sizeof(foo);
	iov[1].iov_base = bar;
	iov[1].iov_len = sizeof(bar);
	iov[2].iov_base = baz;
	iov[2].iov_len = sizeof(baz);

	nr = readv(fd,iov,3);
	if(nr == -1){
		perror("readv");
		return 1;
	}

	for(i = 0; i < 3; i++){
		printf("%d: %s",i,(char *)iov[i].iov_base);
	}

	if(close(fd)){
		perror("close");
		return 1;
	}
	return 0;
}

3、The Event Poll Interface
poll()和select()需要一些描述符列表进行监控,内核需要遍历每一个描述符进行监控,这样如果
描述符列表比较大,就会产生性能问题。
epoll解决了这个问题,它将监控的注册和实际的监控进行了解耦,一个系统调用用来初始化epoll的
环境,另一个用来添加或者删除描述符,第三个进行实际的事件等待。
1)创建一个新的Epoll实例:
epoll_create:
#include <sys/epoll.h>

int epoll_create(int size);

创建了一个epoll实例,返回一个文件描述符,这个文件描述符不和一个实际的文件相关,只是被后面的系统调用
所用到。size是提供给内核要监控多少个文件描述符的一个线索,并不是最大值,提供一个大约的值可以提高性能。
失败了返回-1,并设置errno。EINVAL表示size参数不是正数,ENFILE表示达到了打开文件的最大值,ENOMEM内存不足。
例子:
int epfd;

epfd = epoll_create(100);
if(epfd < 0)
	perror("epoll_create");

2) 控制epoll:
epoll_ctl可以从epoll的环境中添加和删除文件描述符。
#include <sys/epoll.h>

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

epoll_event结构:
struct epoll_event{
	__u32 events;/* events */
	union{
		void *ptr;
		int fd;
		__u32 u32;
		__u64 u64;
	}data;
};

op指定了与fd相关要执行的操作,event参数进一步描述了操作的行为。
op值:
EPOLL_CTL_ADD:添加一个监控的文件描述符到有epfd关联的epoll中。
EPOLL_CTL_DEL:从epfd关联的epoll中删除一个监控的文件描述符。
EPOLL_CTL_MOD: 修改有fd表示的文件描述符。
epoll_event结构中:
events可以是以下值的OR:
EPOLLERR: 文件发生错误,这个事件始终监控,即使没有指定。
EPOLLET:使得监控文件的edge-triggered行为可用,和Level-triggered events对应。
默认是Level-triggered
EPOLLHUP:文件hangup发生。这个事件始终监控,即使没有指定。
EPOLLIN:文件可以无需阻塞的进行读操作。
EPOLLONESHOT:当一个事件被生成和读取,这个文件自动不再被监控,需要重新通过EPOLL_CTL_MOD设置监控。
EPOLLOUT:文件可以无需阻塞的进行写操作。
EPOLLPRI:存在紧急的out-of-band数据需要读。
成功返回0,失败返回-1,并设置好errno,
EBADF:epfd不是一个合法的epoll实例。
EEXIST:EPOLL_CTL_ADD操作,fd已经存在。
ENOENT:EPOLL_CTL_ADD,EPOLL_CTL_DEL操作,不存在相关的fd。
EINVAL:epfd不是epoll的一个实例。
ENOMEM:没有足够的内存。
EPERM:fd不支持epoll
例子:
添加
struct epoll_event event;

int ret;

event.data.fd = fd;
event.events = EPOLLIN | EPOLLOUT;

ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
if(ret)
	perror("epoll_ctl");

修改:
struct epoll event event;
int ret;

event.data.fd = fd;
event.events = EPOLLIN;

ret = epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&event);
if(ret)
	perror("epoll_ctl");

删除:
struct epoll_event event;
int ret;

ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&event);
if(ret)
	perror("epoll_ctl");

3)使用epoll等待事件:
epoll_wait:
#include <sys/epoll.h>

int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

epoll_wait等待与epfd相关联的文件描述符的事件直到timeout微妙。
timeout是0,则立即返回,timeout是-1等待直到一个事件发生。
成功,events指针指向了epoll_event结构,描述了每一个事件,最大到maxevents个事件。
失败返回-1,并设置errno:
EBADF:epfd不合法的文件描述符。
EFAULT:进程没有对有events指向的内存的写访问权限。
EINTR:进程被信号中断。
EINVAL:epfd是个不合法的epoll实例。
例子:
#define MAX_EVENTS 64

struct epoll_event *events;
int nr_events,i,epfd;

events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if(! events ){
	perror("malloc");
	return 1;
}

nr_events = epoll_wait(epfd,events,MAX_EVENTS,-1);
if(nr_events < 0){
	perror("epoll_wait");
	free(events);
	return 1;
}

for(i = 0; i < nr_events;i++){
	printf("event=%ld on fd=%d\n",events[i].events,events[i].data.fd);
	/*
	 *现在可以使用events[i].data.fd进行无阻塞的操作了。
	 */
}

free(events);

4)边缘出发(Edge-Triggered Events) VS 条件触发(Level-Triggered Events):
在epoll_ctl中,如果epoll_event结构中的events参数中设置为EPOLLET,则监控为Edge-Triggered,
否则是Level-Triggered,默认是Level-Triggered。Level-Triggered和poll、select的行为一致,这是
很多开发者期望的方式。使用非阻塞编程可以使用Edge-Triggered。
不同点:
考虑下面使用pipe通讯的生产者消费者例子:
1)生产者写1kb到管道中。
2)消费者使用epoll_wait,等待管道数据到来。
如果使用Level-Triggered,epoll_wait会立即返回,因为已经有数据到来。
如果使用Edge-Triggered,epoll_wait可能会阻塞,如果没有写操作继续的话,因为Edge-Triggered只有在监控的文件的事件发生的时候分发事件。
而2)步是在生产者写之后去epoll_wait的。
4、将文件映像到内存:
内核提供了一个接口,可以将文件映像到内存的buffer中,这样当我们从buffer中获取数据时,相应的文件数据就会被读取,类似,当我们存储数据
到内存中时,相应的数据自动写入文件。
1)mmap:
#include <sys/mmap.h>

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

addr:建议内核将文件映射到的地址,这只是一个hint,为了可移植性,一般设置为0,调用返回mapping开始的实际地址。
fd:要映像文件的描述符。
prot描述了内存映像的保护权限,可以使用OR连接以下选项:
PROT_READ:区域可读。
PROT_WRITE:区域可写
PROT_EXEC:区域可执行
PROT_NONE:区域不可访问,很少有用。
prot不能和打开文件的模式冲突。比如打开了一个只读文件,prot不可以指定为PROT_WRITE。
flags:描述了内存映像的方式,可以使用OR连接以下选项:
MAP_FIXED: addr是必须的,并且不是一个hint,如果内核无法在该地址映像,则失败。
具有不可移植性,不推荐使用。
MAP_PRIVATE:映像不共享,文件被映像为copy-on-write,任何在内存中的改变,都不会反映在文件或者其他进程的mapping中。
MAP_SHARED:映像和其他进程共享映射的同一个文件,写入buffer等效于写入文件,读取映像写操作同时反映在其他进程中。
MAP_SHARE和MAP_PRIVATE其中之一必须被指定。
addr和off的值需要被指定为系统虚拟页的整数倍,可以通过使用_SC_PAGESIZE或_SC_PAGE_SIZE作为sysconf参数获得页的大小。
如果文件的大小12字节,但系统页的大小是512字节,那么系统调用会映射512字节,其他的部分被填充为0,我们可以修改另外的
500个字节,但是并不反应在文件中,所有我们不能使用mmap来append文件,我们需要首先增大文件到指定的值。
sysconf:
#include <unistd.h>

long sysconf(int name);

获得页的大小:
long page_size = sysconf (_SC_PAGESIZE);

linux提供了getpagesize():
#include <unistd.h>

int getpagesize(void);

并不是所有的Unix系统都提供这个方法,POSIX标准已经将它抛弃掉了。
mmap()成功返回mapping的地址,失败返回MAP_FAILED,并设置errno:
EACESS:fd不是普通文件或者fd的模式和prot、flags冲突。
EAGAIN:文件被文件锁锁定。
EBADF:文件描述符不合法。
EINVAL:addr、len或者off不合法
ENFILE:达到了系统最大打开文件数
ENODEV:文件所在的文件系统不支持mmap。
ENOMEM:进程没有足够的内存。
EOVERFLOW:addr+len大于地址空间
EPERM:PROT_EXEC被指定,但是文件系统被mount成noexec。
两个相关的信号:
SIGBUS:进程访问一个已经不合法的映射,例如,文件映射之后被truncated
SIGSEGV:试图往被映射成只读的区域写。
2)munmap:
删除有mmap()创建的映像:
munmap:
#include <sys/mman.h>

int mummap(void *addr, size_t len);

mummap删除了从addr地址开始,是page对齐的,连续的lenzijie的映像,一旦被删除,内存中的区域
就不在有效,访问会产生SIGSEGV信号。
munmap通常传入由mmap返回的addr以及设置的len长度。
成功返回0,失败返回-1,并设置errno:EINVAL,表示有一个或者更多的参数不正确。
if(munmap(addr,len) == -1)
	perror("munmap");

内存映射例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(int argc,char *argv[]){
	struct stat sb;
	off_t i;
	char *p;
	int fd;

	if(argc < 2){
		fprintf(stderr,"usage: %s <file>\n",argv[0]);
		return 1;
	}

	fd = open(argv[1],O_RDONLY);
	if(fd == -1){
		perror("open");
		return 1;
	}

	if(fstat(fd,&sb) == -1){
		perror("fstat");
		return 1;
	}

	if(! S_ISREG(sb.st_mode)){
		fprintf(stderr,"%s is not a regular file\n",argv[1]);
		return 1;
	}

	p = mmap(0,sb.st_size, PROT_READ,MAP_SHARED,fd,0);
	if(p == MAP_FAILED){
		perror("mmap");
		return 1;
	}

	if(close(fd) == -1){
		perror("close");
		return 1;
	}

	for(i = 0;i < sb.st_size; i++){
		putchar(p[len]);
	}

	if(munmap(p,sb.st_size) == -1){
		perror("mummap");
		return 1;
	}
	return 0;
}

3)mmap的优缺点:
优点:
1、从内存映像文件中读写,避免了read、write多余的拷贝。
2、从内存映像文件中读写,避免了多余的系统调用和用户-内核模式的切换
3、可以多个进程共享内存映像文件。
4、seeking内存映像只需要指针操作,避免系统调用lseek。
缺点:
1、内存映像需要时整数倍页大小,如果文件较小,会浪费内存。
2、内存映像需要在放在进程地址空间,大的内存映像可能导致地址空间碎片,找不到足够大的空余连续区域供其它用。
3、内核需要维护更多的和内存映像相关的数据结构。
4)重新设置映像大小:
#define _GNU_SOURCE

#include <unistd.h>
#include <sys/mman.h>

void *mremap(void *addr, size_t old_size, size_t new_size, unsigned long flags);

mremap扩展或者缩小内存映像,从区域[addr,add+old_size]编程一个新的大小new_size
flags:
0:不可以移动来改变内存映像大小。
MREMAP_MAYMOVE:如果需要可以移动地址改变内存映像大小。
返回值:成功返回重新设置大小之后的内存映像的大小,失败返回MAP_FAILED,并设置errno:
EAGAIN:内存区域被锁定,无法改变大小。
EFAULT:给定区域的一些也有不合法的页或者重新映像存在问题。
EINAL:参数不合法。
ENOMEM:不移动则无法扩展,如果MREMAP_MAYMOVE没有被设置。
glibc经常使用mremap来实现高效的realloc():
void * realloc(void *addr, size_t len){
	size_t old_size = look_up_mapping_size(addr);
	void *p;
	p = mremp(addr,old_size, len,MREMAP_MAYMOVE);
	if(p == MAP_FAILED)
		return NULL;
	return p;
}

5)改变映像的protection:
mprotect可以允许程序改变已经存在内存区域的permissions:
#include <sys/mman.h>

int mprotect(const void *addr, size_t len, int prot);

一些系统只能改变由mmap得到的内存映像的protection,Linux可一个操作任何一个内存区域。
成功返回0,失败返回-1,并设置errno为:
EACCESS:不可以被设置成prot的permissions,可能是文件打开时只读的,设置成可写
EINVAL:参数不合法。
ENOMEM:内核内存不足或者所给的内存区域不是进程的合法地址空间。
6)将文件和映像同步:
POSIX提供了和文件操作中fsync类似的将文件和内存映像同步的操作:
msync:
#include <sys/mman.h>

int msync(void *addr, size_t len, int flags);

将内存映像flush到磁盘。没有msync,没有能够保证将mapping的脏数据写回磁盘,除非被unmapped。
当修改内存映像,进程直接修改在内核页cache中的文件页,内核可能不会很快将内核的页cache同步到
磁盘。
flags使用OR链接下面选项:
MS_ASYNC:异步的执行同步操作,msync立即返回,更新操作被调度。
MS_INVALIDATE:指定所有其他的内存映像cache副本无效。任何以后的操作都同步到磁盘中。
MS_SYNC:同步的执行同步操作,等将内容写入磁盘在返回。
if(msync(addr,len,MS_ASYNC) == -1)
	perror("msync");

成功返回0,失败返回-1,并设置errno:
EINVAL:MS_SYNC和MS_ASYNC同时被设置或者addr没有页对齐。
ENOMEM:所给的内存区域(或者部分)没有被映射。
7)给mapping建议:
Linux提供了madvice来让进程建议内核或者给内核提供线索来使用mapping,这样可以优化mapping的使用。
#include <sys/mman.h>

int madvice(void *addr, size_t len, int advice);

len如果为0,内核将建议施用于从addr开始的整个映像。
advice:可以是以下之一:
MADV_NORMAL:没有特别的建议。建议使用中等程度的预先读。
MADV_RANDOM:以随机访问的方式访问指定的区域。建议较少的预先读。
MADV_SEQUENTIAL:顺序访问指定区域。建议大量预先读
MADV_WILLNEED:将来要访问指定区域。初始化预先读,将指定的页读到内存。
MADV_DONTNEED:将来不再访问指定区域。内核释放与指定页关联的资源。后续的读会导致从文件中再度调入。
int ret;

ret = madvise(addr,len,MADV_SEQUENTIAL);
if(ret < 0)
	perror("madvice");

参考:
1、《Linux system programming》
2、《Unix system programming》
3、《Advanced Programming in the Unix Environment》
2
0
分享到:
评论
1 楼 yanqingluo 2013-06-03  
好文章,转走了啊.

相关推荐

Global site tag (gtag.js) - Google Analytics