概述
根据维基百科的定义,IO 指的是输入输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。简而言之,从硬盘中读写数据或者从网络上收发数据,都属于IO行为。 个人的理解,unix系统对于IO在概念上有基础IO行为,指的是那些对文件进行基础的操作行为,如下:
int open(const char *path, int oflag, ...);
int creat(const char *path, mode_t mode);
int close(int fd);
int read(int fd, char *buf, int n );
ssize_t write(int fd, const void *buf, size_z nbytes);
高级IO可以基于基础IO去实现的,它包含很多内容,如非阻塞IO、记录锁、IO多路转接(select和poll函数)、异步IO、readv和writev函数以及存储映射IO(mmap)等。
非阻塞IO
非阻塞IO使我们可以发出open、read和write这样的IO操作,并使这些操作不会永远阻塞。 它的特点是:
- 进程轮询(重复)调用,消耗CPU资源。
- 实现难度低,开发应用相对阻塞IO模式较难。
- 适用并发量较小,且不需要及时响应的网络应用开发。
对于一个给定的文件描述符,有两种为其指定非阻塞IO的方法:
- 如果调用open获得描述符,则可指定O_NONBLOCK标志。(参数path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则后续的IO操作都是非阻塞的)
- 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
fcntl函数可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性,它的功能依据cmd的值的不同而不同,cmd通用的参数对应功能如下:
- F_DUPFD:与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。
- F_GETFD:读取文件描述符close-on-exec标志。
- F_SETFD:设置文件描述符close-on-exec标志,设置为第三个参数arg的最后一位。
- F_GETFL:获取文件打开方式的标志,标志值含义与open调用一致。
- F_SETFL:设置文件打开方式的标志,arg为指定方式。
- F_GETLK:获取记录锁,来确定一个文件区域的加锁状态。lock指定为锁的类型结构。
- F_SETLK:设置记录锁,lock指定为锁的类型结构。加锁或解锁,成功返回l_type为预置值。
- F_SETLKW:与F_SETLK一样,但阻塞等待返回。
例子
下面先给出阻塞的例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
char buf[10];
printf("--- 准备接收数据 ---\n");
int n = read(0,buf,10);
write(STDOUT_FILENO, buf, n);
return 0;
}
非阻塞:
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int flag = fcntl(0,F_GETFL);
fcntl(0,F_SETFL,flag|O_NONBLOCK);
char buf[10];
while(1)
{
int n = read(0,buf,10);
if(n < 0 && errno == EAGAIN)
{
printf("--- 还未接收到数据 ---\n");
sleep(2);
continue;
}else{
write(STDOUT_FILENO, buf, n);
return 0;
}
}
return 0;
}
记录锁
当两个人同时编辑一个文件时,其后果将如何呢?在大多数UNIX系统中,该文件最后状态取决于写该文件的最后一个进程。但是对于有些应用程序,如数据库,进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。 记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。“记录”这个词是一种误用,更适合的术语可能是“字节范围锁”(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
记录锁也是基于fcntl函数实现,其第3个参数是指向flock结构的指针,代表着锁的类型:
struct flock {
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
对flock结构说明如下:
- l_type:所希望的锁类型,有F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁)。原锁为读锁时,在同文件域可设读锁;原锁为写锁,在同文件域不可设读/写锁。
- l_whence:要加锁或解锁区域的位置:开头SEEK_SET、当前位置SEEK_CUR、末尾SEEK_END。
- l_start:起始字节偏移量。
- l_len:区域的字节长度。
- l_pid:表明该进程的ID持有的锁能阻塞当前进程。
当l_len的值为0时,则表示锁的区域从起点开始直至最大的可能位置,就是从l_whence和l_start两个变量确定的开始位置开始上锁,将开始以后的所有区域都上锁。
锁的隐含继承和释放
关于记录锁的自动继承和释放有三条规则:
- 锁与进程和文件两方面有关。这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
- 由fork产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程需要调用fcntl才能获得它自己的锁。这与锁的所用是相一致的。锁的作用是阻止多个进程同时写一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。
- 在执行exec函数后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了close-on-exec标志,那么当作为exec的一部分关闭该文件描述符时,对相应文件的所有锁都被释放了。
死锁
当内核检测到死锁时,系统都会选择性地让进程接收错误返回,至于让哪些进程进行这一步每个系统都是不同的。
例子1
在一个进程对文件设置读锁时,其他进程无法对文件设置写锁,但可以设置读锁。
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("record_lock.txt", O_RDWR | O_CREAT);
struct flock lock;
lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) != 0)
{
printf("error %d\n",lock.l_type);
return -1;
}
sleep(1000);
return 0;
}
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("record_lock.txt", O_RDWR | O_CREAT);
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) != 0)
{
printf("error %d\n",lock.l_type);
return -1;
}
sleep(1000);
return 0;
}
例子2
尝试对文件某个区域进行设置写锁。进程1对0-9区域进行设置写锁,进程2对2-5区域进行设置写锁,结果失败,进程3对11-无限区域进行设置写锁,结果成功。
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("record_lock.txt", O_RDWR | O_CREAT);
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 10;
if (fcntl(fd, F_SETLK, &lock) != 0)
{
printf("error %d\n",lock.l_type);
return -1;
}
sleep(1000);
return 0;
}
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("record_lock.txt", O_RDWR | O_CREAT);
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 2;
lock.l_len = 4;
if (fcntl(fd, F_SETLK, &lock) != 0)
{
printf("error %d\n",lock.l_type);
return -1;
}
sleep(1000);
return 0;
}
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd = open("record_lock.txt", O_RDWR | O_CREAT);
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 10;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) != 0)
{
printf("error %d\n",lock.l_type);
return -1;
}
sleep(1000);
return 0;
}
IO多路转接
非阻塞IO很简单,当你调用 read 时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入,所以就有了IO多路转接(也叫IO多路复用)。它先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行IO时,该函数才返回,在返回时,它告诉进程哪些描述符已准备好可以进行IO。 底层实现都是维护等待队列,根据队列元素绑定进程,然后通过数据的是否准备妥当对进程进行休眠与唤醒达到目的。 select、pselect和poll这三个函数使我们能够执行IO多路转接。
select
在所有依从POSIX的平台上,select函数使我们可以执行IO多路转接。传向select的参数告诉内核:
- 我们所关心的描述符。
- 对于每个描述符我们所关心的状态。(是否读一个给定的描述符?是否想写一个给定的描述符?是否关心一个描述符异常状态?)
- 愿意等待多长时间(可以永远等待,等待一个固定量时间或完全不等待)。
从select返回时,内核告诉我们:
- 已准备好的描述符的数量。
- 对于读、写或异常这三个状态中的每一个,哪些描述符已准备好。
#include <sys/select.h>
int select(int maxfdp1,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *tvptr);
先说明最后一个参数,它指定愿意等待的时间:
struct timeval{
long tv_sec;
long tv_usec;
}
有三种情况:
- tvptr == NULL:永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR
- tvptr->tv_sec == 0 && tvptr->tv_usec == 0:完全不等待。测试所有指定的描述符并立即返回。
- tvptr->tv_sec != 0 || tvptr->tv_usec != 0:等待指定的秒数和微妙数。当指定的描述符之一已准备好或当指定的时间值已经超过时立即返回。如果在超时时还没有一个描述符准备好,则返回值是0。与第一种情况一样,这种等待可被捕捉到的信号中断。
中间三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种数据类型为每一可能的描述符保持了一位。 对fd_set数据类型可以进行的处理是:分配一个这种类型的变量;将这种类型的一个变量值赋予同类型的另一个变量;或对于这种类型的变量使用下列四个函数中的一个。
#include <sys/select.>
int FD_ISSET(int fd,fd_set *fdset);
void FD_CLR(int fd,fd_set *fdset);
void FD_SET(int fd,fd_set *fdset);
void FD_ZERO(fd_set *fdset);
- 调用FD_ZERO将一个指定的fd_set变量的所有位设置为0.
- 调用FD_SET设置一个fd_set变量的指定位。
- 调用FD_CLR则将一指定位清除。
- 调用FD_ISSET测试一指定位是否设置。
select的中间三个参数(指向描述符集的指针)中的任意一个或全部都可以是空指针,这表示对相应状态并不关心。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。
select的第一个参数maxfdp1的意思是“最大描述符加1”。在三个描述符集中找出最大描述符值,然后加1,这就是第一个参数。也可以将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常量,它说明了最大的描述符(经常是1024)。如果将第一个参数设置为我们关注的最大描述符编号值加1,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。
例子
select读取控制台输入并输出,五秒打印超时。
#include <stdio.h>
#include <sys/select.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
int main(void)
{
int keyboard;
int ret, i;
char c;
fd_set readfd;
struct timeval timeout;
keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(keyboard <= 0)
{
perror("open terminal error");
}
while(1)
{
timeout.tv_sec = 5;
timeout.tv_usec = 0;
FD_ZERO(&readfd);
FD_SET(keyboard, &readfd);
ret = select(keyboard + 1, &readfd, NULL, NULL, &timeout);
if(ret == -1)
{
perror("select error");
}
else if(ret)
{
if(FD_ISSET(keyboard, &readfd))
{
i = read(keyboard, &c, 1);
if ('\n' == c)
{
continue;
}
printf("你输入的是: %c\n", c);
}
}
else if(ret == 0)
{
printf("-- 超时5秒,进入下个5秒 --\n");
}
}
}
pselect
POSIX.1也定义了一个select变体,就是pselect:
#include <sys/select.h>
int pselect(int maxfdp1,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,
const struct timespec *tsptr,const sigset_t *sigmask);
pselect相对于select的两个变化:
- pselect使用timespec结构,能指定到纳秒级(旧结构只能指定到微秒级)
- pselect增加了指向信号集的指针sigmask(此时的信号集表示信号掩码)
也就是说,pselect函数是一个 防止信号干扰的增强型 select函数,对于pselect可使用一可选择的信号屏蔽字。若sigmask为空,那么在与信号有关的方面,pselect的运行状况和select相同。否则,sigmask指向一信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时恢复以前的信号屏蔽字。
例子
在select的例子上做点小改动,测试下信号屏蔽字的作用。添加一个alarm的闹钟,3秒就会触发SIGALRM信号,然后sigmask里设置SIGALRM信号,这样的话,调用pselect就不会被这个信号打断了。
#include <stdio.h>
#include <sys/select.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <signal.h>
int main(void)
{
int keyboard;
int ret, i;
char c;
fd_set readfd;
sigset_t sigmask;
sigset_t sigset;
keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(keyboard <= 0)
{
perror("open terminal error");
}
if (sigemptyset(&sigset) == -1)
{
perror("sigemptyset error");
}
if (sigaddset(&sigset, SIGALRM) == -1)
{
perror("sigaddset error");
}
alarm(3);
while(1)
{
FD_ZERO(&readfd);
FD_SET(keyboard, &readfd);
ret = pselect(keyboard + 1, &readfd, NULL, NULL, NULL, &sigset);
if(ret == -1)
{
perror("pselect error");
}
else if(ret)
{
if(FD_ISSET(keyboard, &readfd))
{
i = read(keyboard, &c, 1);
if ('\n' == c)
{
continue;
}
printf("你输入的是: %c\n", c);
}
}
else if(ret == 0)
{
printf("-- 超时5秒,进入下个5秒 --\n");
}
}
}
poll
poll函数跟select差不多,但是接口是有所不同的,它可以用于任何类型的文件描述符。
#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
与select不同,poll不是为每个状态(可读性、可写性和异常状态)构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其关心的状态。 每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。
struct pollfd{
int fd;
short events;
short revents;
}
nfds为fdarray的数组数量。 应将每个数组元素的events成员设置为下图中所示值的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。poll函数并没有和select一样更改events成员。 下图中前4行测试的是可读性,接下来的3行测试的可写性,最后3行测试的异常条件。
最后三行是由内核在返回时设置的。即使在events字段中没有指定这三个值,如果相应条件发生,则在revents中也返回它们。 POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。 例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
poll的最后一个参数说明我们愿意等待时间。有三种不同的情形:
- timeout == -1 永远等待。当所指定的描述符中的一个已准备好,或捕捉到一个信号时则返回。如果捕捉到一个信号,则poll返回-1,errno设置为EINTR。
- timeout == 0 不等待。
- timeout > 0 等待timeout毫秒。当指定的描述符之一已准备好,或指定的时间值已超过时立即返回。如果已超时,但是还没有一个描述符准备好,则返回值是0。
异步IO
POSIX异步IO为不同类型的文件进行异步IO提供了一套一致(接口函数)的方法。
AIO控制块
这些异步IO接口函数使用AIO控制块来描述IO操作。aiocb结构体定义了AIO控制块,它至少包括以下字段:
struct aiocb
{
int aio_fildes;
off_t aio_offset;
int aio_lio_opcode;
int aio_reqprio;
volatile void * aio_buf;
size_t aio_nbytes;
struct sigevent aio_sigevent;
};
- 注意,异步IO操作必须显式的指定偏移量aio_offset,因为异步IO接口并不影响(或使用)由操作系统维护的文件表项中记录的偏移量。
- 如果使用异步IO向一个以追加模式(O_APPEND)open的文件中写入数据,则aio_offset字段被忽略。
- aio_reqprio:是异步IO请求提示顺序(请求优先级)。但是系统对于该顺序只有有限的控制力,即不一定遵循。
- aio_lio_opcode:只能用于基于列表的异步IO(仅被 lio_listio() 函数使用)
- aio_sigevent:控制IO事件完成后,如何通知应用程序,通过sigevent结构体描述
sigevent结构体:
struct sigevent
{
int sigev_signo;
int sigev_notify;
union sigval sigev_value;
void(*sigev_notify_function)(union sigval);
pthread_attr_t * sigev_notify_attributes;
};
sigev_notify取值为以下三个中的一个:
- SIGEV_NONE:异步IO请求完成后,不通知进程
- SIGEV_SIGNAL:异步IO请求完成后,产生由sigev_signo字段指定的信号。如果应用程序要捕获该信号,且在sigaction时指定了SA_SIGINFO标志,那么该信号将被入队(如果支持排队信号)。信号处理程序sa_sigaction中第二个参数siginfo_t中的si_value字段值被置为 sigev_value。
- SIGEV_THREAD:当异步IO请求完成时,调用sigev_notify_function函数。该函数的sigval参数值为sigev_value。系统会自动将该函数在分离状态下的一个单独的线程中执行(该线程的属性是sigev_notify_attributes)。
异步读写
在初始化了aiocb结构体后,就可以调用aio_read函数来进行异步读操作,或调用aio_write函数来进行异步写操作。
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
- 当函数返回成功时,异步I/O请求便已经被操作系统放入等待处理的队列中了。
- 这些返回值与实际I/O操作的结果没有任何关系。
- I/O操作在等待时,必须保证aiocb对象中的缓冲区等资源始终是可用的。
同步读写
如果想强制(排队中)等待的异步操作不等待,而直接写入持久化的存储中,可以建立一个 AIO 控制块并调用 aio_fsync 函数。 注意,同aio_read和aio_write一样,该函数只是一个请求(即一个同步请求),它并不等待I/O结束(只是将同步请求入队),而是立即返回
int aio_fsync(int op, struct aiocb *aiocbp);
阻塞
可以通过aio_suspend函数阻塞进程,直到异步I/O操作结束取消
int aio_suspend(const struct aiocb * const aiocb_list[],int nitems,
const struct timespec *timeout);
- aiocb_list:阻塞的异步I/O操作数组,数组元素必须指向已用于初始化异步I/O操作的aiocb控制块。
- nitems:阻塞的异步I/O操作数组元素个数
- timeout:等待的时间,NULL代表永远等待
返回值:
- 被信号中断返回-1,errno置位EINTR
- 超过timeout时间限制,返回-1,errno置位EAGAIN
- 如果有任何异步I/O操作完成,该函数返回0。
- 如果调用该函数时所有异步I/O操作已经完成,则不阻塞直接返回
取消
当我们不想再完成等待中的异步I/O操作时,可以尝试用aio_cancel函数取消它们。 注意,系统无法保证能够取消该异步I/O操作,所以是尝试。
int aio_cancel(int fd, struct aiocb *aiocbp);
返回值:
- AIO_ALLDONE:所有操作在尝试取消它们之前已经完成
- AIO_CANCELED:所有要求的操作已被取消
- AIO_NOTCANCELED:至少一个要求的操作没有被取消
- -1:调用失败,置位errno
获取异步的完成状态
获知一个异步读、写或者同步操作的完成状态,可以通过aio_error函数:
int aio_error(const struct aiocb *aiocbp);
返回值:
- 0:异步操作成功完成,需要调用aio_return函数获取操作返回值
- -1:失败,并置位errno
- EINPROGRESS:异步读、写或同步操作仍在等待
- 其他情况:相关异步操作失败返回的错误码。如ECANCELED代表该异步I/O被取消了
获取异步的目标返回值
如果异步操作成功,可以调用aio_return函数来获取异步操作的返回值:
ssize_t aio_return(struct aiocb *aiocbp);
- 异步操作完成之前,都不要调用该函数
- 异步操作完成之后,对每个异步操作只能调用一次该函数。因为一旦调用了该函数,操作系统就释放掉了包含I/O操作返回值的记录。
返回值:
- -1:调用失败,置位errno
- 其他情况:返回异步操作结果,即会返回read、write或者fsync在被成功调用时可能返回的结果
readv和writev函数
如果要从文件中读一片连续的数据至进程的不同区域,有两种方案:
- 使用read()一次将它们读至一个较大的缓冲区中,然后将它们分成若干部分复制到不同的区域;
- 调用read()若干次分批将它们读至不同区域。
同样,如果想将程序中不同区域的数据块连续地写至文件,也必须进行类似的处理。 UNIX提供了另外两个函数—readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base;
size_t iov_len;
};
存储映射IO
存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上。于是当从缓冲区中取数据时,就相当于读文件中的相应字节;将数据存入缓冲区时,相应字节就自动写入文件。就可以在不使用read和write的情况下执行I/O。
mmap函数
mmap函数告诉内核将一个给定的文件映射到一个存储区域中。 mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。 存储映射文件示意图:
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- addr:指定映射存储区起始地址(即进程中的一块内存区域)。如果为NULL,则由系统选择该映射区起始地址,且函数返回值是该地址。
- length:映射区的长度(字节)。可以通过stat系统调用获得打开文件的大小信息,然后设置为这个参数
- prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以是PROT_NONE ,也可以是其他三个值得按位或。
- PROT_EXEC 映射区可以被执行
- PROT_READ 映射区可以被读取
- PROT_WRITE 映射区可以被写入
- PROT_NONE 映射区不可访问
- flag:指定映射对象的类型,映射选项和映射页是否可以共享。下面是常用的类型:
- MAP_FIXED:返回值必须等于addr参数。该标志不建议使用。因为如果addr非0,则内核只把该参数当做一种建议,但是不保证会使用所要求的地址。因此建议将addr设置为NULL
- MAP_PRIVATE:多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制)的映射方式。即内存区域的写入不会影响到原文件
- MAP_SHARED:多进程间数据共享,修改反应到磁盘实际文件中,相当于输出到文件
- fd:要被映射的文件描述符。必须先打开该文件。
- offset:被映射内容的起点(即fd文件内容偏移量)
与read、write对比
read和write执行了更多的系统调用,并做了更多的复制。read和write将数据从内核缓冲区中复制到应用缓冲区,然后再把数据从应用缓冲区复制到内核缓冲区。而mmap则直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。 所以他们两者的效率的比较就是系统调用和额外的复制操作的开销和页错误的开销之间的比较,哪一个开销少就是哪一个表现更好。当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误的结果而出现(每次错页读发生一次错误,每次错页写发生一次错误)。 用mmap可以避免与读写打交道,这样可以简化程序逻辑,有利于编程实现。
|