非阻塞I/O

阻塞I/O对应于低速的系统调用,可能会使进程永远阻塞。非阻塞I/O可以使我们发出open、read、write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即错误返回,其表示该操作如果继续执行将会阻塞。

对于一个给定的描述符,有两种为其指定非阻塞I/O的方法:

(1)   如果是调用open获得文件描述符,则可指定O_NONBLOCK标志。

(2)   对于一个已经打开的文件描述符,则可以调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

如下是一个非阻塞I/O的实例,它从标准输入读500000个字节,并试图将它们写到标准输出上。该程序先将标准输出设置为非阻塞的,然后用for循环进行输出,每次write调用的结果都在标准错误上打印。

 [root@benxintuzi IOpri]# cat nonblock.c
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h> char buf[]; void set_fl(int fd, int flags);
void clr_fl(int fd, int flags); int main(void)
{
int ntowrite, nwrite;
char* ptr; ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
fprintf(stderr, "read %d bytes\n", ntowrite); set_fl(STDOUT_FILENO, O_NONBLOCK); /* set nonblocking */ ptr = buf;
while (ntowrite > )
{
errno = ;
nwrite = write(STDOUT_FILENO, ptr, ntowrite);
fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno); if (nwrite > )
{
ptr += nwrite;
ntowrite -= nwrite;
}
} clr_fl(STDOUT_FILENO, O_NONBLOCK); /* clear nonblocking */ return ;
} void set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
int val; if ((val = fcntl(fd, F_GETFL, )) < )
printf("fcntl F_GETFL error\n"); val |= flags; /* turn on flags */ if (fcntl(fd, F_SETFL, val) < )
printf("fcntl F_SETFL error\n");
} void clr_fl(int fd, int flags) /* flags are the file status flags to turn off */
{
int val; if ((val = fcntl(fd, F_GETFL, )) < )
printf("fcntl F_GETFL error\n"); val &= ~flags; /* turn flags off */ if (fcntl(fd, F_SETFL, val) < )
printf("fcntl F_SETFL error\n");
} [root@benxintuzi IOpri]# gcc nonblock.c -o nonblock
[root@benxintuzi IOpri]# ./nonblock < nonblock.c > temp.file
read bytes
nwrite = , errno =
[root@benxintuzi IOpri]# ls -l temp.file
-rw-r--r--. root root Sep : temp.file

记录锁

在大多数的Unix系统中,当两个人同时编辑同一个文件时,该文件的最后状态取决于写该文件的最后一个进程。但是在某些应用程序中,如数据库,进程有时必须确保它正在单独写一个文件,为了向进程提这种功能,Unix系统提供了记录锁机制。

记录锁(record locking的功能是:当第一个进程正在读或写文件的某个部分时,使用记录锁可以阻止其他进程修改同一个文件区块。要注意的是,锁定的只是一个文件区块,而不绝对是整个文件(当然也可能是整个文件)。

POSIX.1标准加锁机制的基础是fcntl函数:

#include <fcntl.h>

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

返回值:成功,依赖于cmd;失败,返回-1

说明:

cmd可取F_GETLK、F_SETLK、F_SETLKW。

flockptr是一个指向flock结构体的指针:

struct flock

{

short  l_type;       /* F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域) */

short  l_whence;(加锁或解锁区域的起始字节偏移量)   /* SEEK_SET、SEEK_CUR、SEEK_END */

off_t  l_start; (加锁或解锁区域的起始字节偏移量)   /* offset in bytes, relative to l_whence */

off_t  l_len;(区域的字节长度)      /* length, in bytes; 0 means lock to EOF */

pid_t  l_pid;(进程的ID(l_pid)持有的锁能阻塞当前进程)      /* returned with F_GETLK */

};

锁可以在当前文件尾端或者超过尾端开始,但是不能在文件起始位置前开始。

如果l_len为0,则表示锁的范围可以扩展到最大可能的偏移量。这意味着不管向文件中追加了多少数据,他们都可以处于锁的范围内。

如果想要对整个文件加锁,我们设置l_start和l_whence指向文件的起始位置,并且指定长度l_len为0。

关于共享读锁和独占性写锁,基本规则如下:

任意多个进程在一个给定的区域上可以有共享读锁,但是在一个给定的区域上只能有一个进程拥有一把独占性写锁。

需要注意的是,如果是只有一个进程,那么如果该进程对一个文件区域已经有了一把锁,后来该进程又企图在同一文件区域再加另一把锁,那么新锁将替换掉之前的锁,即使是先有写锁后又读锁,最后也是用读锁来替换写锁,并不会阻塞,切记。

加读锁时,该文件描述符必须是读打开。加写锁时,该文件描述符必须是写打开。

如下详细介绍cmd的3个取值:

F_GETLK: 用于判断由flockptr描述的锁是否会被另外一把锁阻塞。如果已经存在一把锁,并且它阻止创建由flockptr所描述的锁,则该现有锁的信息将被写入到flockptr中;如果不存在,则flockptr指向的结构保持不变。

F_SETLK: 设置由flockptr所描述的锁。如果试图设置锁失败,那么fcntl立即出错,此时errno设置为EACCES/EAGAIN。

F_SETLKW: F_SETLKW的阻塞版本,W表示wait。如果设置锁失败,那么调用进程将睡眠,直至请求创建的锁已经可用,或者睡眠由信号中断,则该进程立即被唤醒。

一般而言,可以用F_GETLK来测试是否能创建一把锁,然后用F_SETLK/F_SETLKW企图建立那把锁,但这两者之间并非一个原子操作。因此不能保证在这两次fcntl调用之间不会有另一个进程插入并建立一把相同的锁。

为了避免每次分配flock结构,然后又填入相关信息,可以用函数lock_reg来处理这些细节:

 int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock; lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */ return(fcntl(fd, cmd, &lock));
}

由于大多数锁调用是为了对一个文件区域加锁或者解锁(实际中F_GETLK很少使用),通常使用下列5个宏中的一个来实现这种功能:

 #define    read_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len)) /* w means wait */
#define write_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))

实际中,测试锁很少用F_GETLK,而是用如下函数lock_test来代替,如果存在该锁,那么它将阻塞由参数指定的锁请求,返回持有当前锁的进程ID,否则,返回0,通常如下两个宏使用lock_test函数:

 #define    is_read_lockable(fd, offset, whence, len) \
(lock_test((fd), F_RDLCK, (offset), (whence), (len)) == )
#define is_write_lockable(fd, offset, whence, len) \
(lock_test((fd), F_WRLCK, (offset), (whence), (len)) == ) pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
struct flock lock; lock.l_type = type; /* F_RDLCK or F_WRLCK */
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */ if (fcntl(fd, F_GETLK, &lock) < )
printf("fcntl error\n"); if (lock.l_type == F_UNLCK)
return (); /* false, region isn't locked by another proc */
return (lock.l_pid); /* true, return pid of lock owner */
}

死锁是由于双方缺少对方拥有的资源而发生的,在进程加锁过程中,如果进程1已经控制了文件中的一个加锁区域,然后它又试图对进程2控制的区域进行加锁,极有可能发生死锁,因为,进程2控制的区域可能已经加了锁,该锁暂时又不会被释放。

有一个死锁的例子:父进程对第一个字节加锁,子进程对第0个字节加锁,然后它们中的一个又试图对对方的加锁区域进程加锁,但是在给出该程序之前,我们先补充一下进程间同步的小知识:

如果多个进程都需要对共享数据进行某种形式的处理,而最终的结果又取决于进程运行的顺序,此时,必须对进程间进行同步控制。一种最简单的控制父子进程运行顺序的方式是:要求每个进程在执行其初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作。用代码描述如下:

 static volatile sig_atomic_t sigflag; /* set nonzero by sig handler */
static sigset_t newmask, oldmask, zeromask; static void sig_usr(int signo) /* one signal handler for SIGUSR1 and SIGUSR2 */
{
sigflag = ;
} void TELL_WAIT(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("signal(SIGUSR1) error\n");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("signal(SIGUSR2) error\n");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSR1);
sigaddset(&newmask, SIGUSR2); /* Block SIGUSR1 and SIGUSR2, and save current signal mask */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < )
printf("SIG_BLOCK error\n");
} void TELL_PARENT(pid_t pid)
{
kill(pid, SIGUSR2); /* tell parent we're done */
} void WAIT_PARENT(void)
{
while (sigflag == )
sigsuspend(&zeromask); /* and wait for parent */
sigflag = ; /* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < )
printf("SIG_SETMASK error\n");
} void TELL_CHILD(pid_t pid)
{
kill(pid, SIGUSR1); /* tell child we're done */
} void WAIT_CHILD(void)
{
while (sigflag == )
sigsuspend(&zeromask); /* and wait for child */
sigflag = ; /* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < )
printf("SIG_SETMASK error\n");
}

死锁实例如下:

 [root@benxintuzi IOpri]# cat deadlock.c
#include <signal.h>
#include <fcntl.h>
#include <stdio.h> int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock; lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */ return(fcntl(fd, cmd, &lock));
} /*
* * Default file access permissions for new files.
* */
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) #define writew_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len)) static volatile sig_atomic_t sigflag; /* set nonzero by sig handler */
static sigset_t newmask, oldmask, zeromask; static void sig_usr(int signo) /* one signal handler for SIGUSR1 and SIGUSR2 */
{
sigflag = ;
} void TELL_WAIT(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("signal(SIGUSR1) error\n");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("signal(SIGUSR2) error\n");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSR1);
sigaddset(&newmask, SIGUSR2); /* Block SIGUSR1 and SIGUSR2, and save current signal mask */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < )
printf("SIG_BLOCK error\n");
} void TELL_PARENT(pid_t pid)
{
kill(pid, SIGUSR2); /* tell parent we're done */
} void WAIT_PARENT(void)
{
while (sigflag == )
sigsuspend(&zeromask); /* and wait for parent */
sigflag = ; /* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < )
printf("SIG_SETMASK error\n");
} void TELL_CHILD(pid_t pid)
{
kill(pid, SIGUSR1); /* tell child we're done */
} void WAIT_CHILD(void)
{
while (sigflag == )
sigsuspend(&zeromask); /* and wait for child */
sigflag = ; /* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < )
printf("SIG_SETMASK error\n");
} static void lockabyte(const char *name, int fd, off_t offset)
{
if (writew_lock(fd, offset, SEEK_SET, ) < )
printf("%s: writew_lock error\n", name);
printf("%s: got the lock, byte %lld\n", name, (long long)offset);
} int main(void)
{
int fd;
pid_t pid; /*
* * Create a file and write two bytes to it.
* */
if ((fd = creat("templock", FILE_MODE)) < )
printf("creat error\n");
if (write(fd, "ab", ) != )
printf("write error\n"); TELL_WAIT(); if ((pid = fork()) < ) {
printf("fork error\n");
} else if (pid == ) { /* child */
lockabyte("child", fd, );
TELL_PARENT(getppid());
WAIT_PARENT();
lockabyte("child", fd, );
} else { /* parent */
lockabyte("parent", fd, );
TELL_CHILD(pid);
WAIT_CHILD();
lockabyte("parent", fd, );
} return ();
} [root@benxintuzi IOpri]# ./deadlock
parent: got the lock, byte
child: got the lock, byte
child: writew_lock error
child: got the lock, byte
parent: got the lock, byte

锁的继承与释放:

关于记录锁的自动继承和释放有3条规则:

1. 锁与进程和文件相关联。当一个进程终止时,其所建立的锁全部释放。同时,无论一个描述符何时    关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放,这意味着如果执行下列4步:

  fd1 = open(pathname, ...);

  read_lock(fd1, ...);

  fd2 = dup(fd1);

  close(fd2);

  在执行close(fd2)后,在fd1上设置的锁将被释放。

2. 由fork产生的子进程不继承父进程所设置的锁。对于通过fork从父进程继承过来的描述符,子进程需要调用fcntl才能获得它自己的锁。这是为了防止父子进程同时写同一个文件。

3. 在执行exec后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了执行了关闭标志,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁。

守护进程可以用一把文件锁来保证只有该守护进程的唯一副本在运行,守护进程可以调用lockfile函数为文件加锁:

 #include <unistd.h>
#include <fcntl.h> int lockfile(int fd)
{
struct flock fl; fl.l_type = F_WRLCK;
fl.l_start = ;
fl.l_whence = SEEK_SET;
fl.l_len = ;
return(fcntl(fd, F_SETLK, &fl));
}

当然也可以用write_lock函数定义lockfile函数:

#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)

I/O多路转接

当从一个文件描述符读,然后又写到另一个文件描述符时,一般在下列形式的循环中使用阻塞I/O:

while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)

if (write(STDOUT_FILENO, buf, n) != n)

printf(“write error\n”);

这种形式的阻塞I/O随处可见,但是如果必须从两个文件描述符读,该如何处理呢?在这种情况下,我们不能在任一个描述符上进行阻塞read,因为可能会因为被阻塞在一个描述符的read操作上而导致另一个描述符即使有数据也无法处理。

一种比较好的技术是使用I/O多路转接(I/O multiplexing)。在这种技术中,先构造一张我们感兴趣的描述符列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数才返回。如下介绍多路转接函数select、pselect、poll。

函数select和pselect

传给select的参数告诉内核:

我们所关心的描述符、关于每个描述符我们所关心的条件、愿意等待多长时间;

内核返回给我们如下信息:

已准备好的描述符的总数量、对于读、写或异常这3个条件中的每一个,哪些描述符已做好准备。

使用这种返回信息,就可调用相应的I/O函数(一般是read或write),并且确知不会发生阻塞情况。

#include <sys/select.h>

int select(int maxfdp1, fd_set* restrict readfds, fd_set* restrict writefds, fd_set* restrict exceptfds, struct timeval* restrict tvptr);

返回值:成功,返回准备就绪的描述符数目;失败,返回0;出错,返回-1。

说明:

tvptr: 指定愿意等待的时间长度,单位为秒和微妙,有3种情况:

tvptr == NULL: 永远等待。当所指定的描述符中的一个已经准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR。

tvptr->tv_sec == 0 && tvptr->tv_usec == 0: 根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞select函数的方式。

tvptr->tv_sec != 0 || tvptr->tv_usec != 0: 等待指定的秒数和微妙数。当指定的描述符已准备好,或当指定的时间值已经超时立即返回。如果在超时前没有任何一个描述符准备好,则返回0;

readfds、writefds、exceptfds是指向描述符集的指针。说明了我们关心的可读、可写、处于异常条件的描述符集合。每个描述符集用一个fd_set数据类型表示(这个数据类型是由实现选择的,它可以为每一个可能的描述符保持一位),如下所示:

Linux I/O 进阶-LMLPHP

例如,如下程序造成的以上三个变量的变化如下:

fd_set readset, writeset;

FD_ZERO(&readset);

FD_ZERO(&writeset);

FD_SET(0, &readset);

FD_SET(3, &readset);

FD_SET(1, &writeset);

FD_SET(2, &writeset);

select(4, &readset, &writeset, NULL, NULL);

Linux I/O 进阶-LMLPHP

对fd_set数据类型的操作函数如下:

#include <sys/select.h>

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在描述符集中,返回非0;否则,返回0

注:

在声明了一个描述符集之后,必须调用FD_ZERO函数将这个描述符集置位0,然后在其中设置我们所关心的各个描述符位:

fd_set rset;

int fd;

FD_ZERO(&rset);

FD_SET(fd, &rset);

FD_SET(STDIN_FILENO, &rset);

从select返回时,可以用FD_ISSET测试该集中的一个给定位是否处于打开状态:

if (FD_ISSET(fd, &rset))

{

....

}

select函数中间的三个参数中的任意一个都可以是空指针,表示对相关条件并不关心,如果3个指针全部都是NULL,则select仅充当了比sleep更精确的定时器而已(sleep指定整数秒,而select可以指定的时间小于1秒)。

maxfdp1的意思是“最大描述符编号加1“。也可以将其设为FD_SETSIZE,这是<sys/select.h>中的一个常量,它指定了最大的描述符数(通常为1024)。但是对于大多数应用程序而言,根本用不了,一般就使用3~10个描述符就足够了,因此指定了具体个数后,内核就只需在此范围内查找打开的位就够了,而不必在此浪费太多的搜索时间。

注:

关于select函数返回值中的“准备好“的含义是对于read/write操作,已经有了一个描述符可以不阻塞地进行。在异常情况下,这意味着有一个描述符存在一个未决条件,一般为网络连接上到达带外数据,或者在处于数据包模式的伪终端上发生了某些条件。

如果在一个描述符上碰到了文件尾端,则select会认为该描述符是可读的,然后调用read,返回0,这是Unix系统指示到达文件尾端的方法(大多数都认为:到达文件尾端时,select会指示一个异常条件)。

补充:

POSIX.1也定义了一个select的变体,称为pselect,如下:

#include <sys/select.h>

int pselect(int maxfdp1, fd_set* restrict readfds, fd_set* restrict writefds, fd_set* restrict exceptfds, const struct timespec* restrict tsptr, const sigset_t* restrict sigmask);

以下为pselect和select的区别:

(1)   select的超时用timeval(秒和微秒)指定,但pselect的超时用timespec(秒和纳秒)指定。

(2)   pselect的超时值被声明为const,保证了调用pselect不会改变此值。

(3)   pselect可使用可选的信号屏蔽字。在信号处理方面,若sigmask为NULL,则pselect与select等效;否则sigmask指向一个信号屏蔽字,在调用pselect时,以原子方式安装该信号屏蔽字,在返回时,恢复以前的信号屏蔽字。

#include <poll.h>

int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

返回值:成功,返回准备好的描述符数目;失败,返回0;出错,返回-1

说明:

与select不同,poll并非为每个条件都构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件:

struct pollfd

{

int    fd;        /* file descriptor to check, or < 0 to ignore */

short  events;       /* events of interest on fd */

short  revents;   /* events that occurred on fd */

};

nfds指定了fdarray数组中的元素数目。

每个数组元素的events成员设置为如下值中的一个或几个,通过这些值告诉内核我们关心的是这个描述符上的哪些事件,返回时,revents成员由内核设置,用于说明每个描述符发生了哪些事件。

标志名

输入至events?

从revents返回?

说明

POLLIN

·

·

可以不阻塞地读高优先级数据以外的数据(相当于POLLRDNORM | POLLRDBAND)

POLLRDNORM

·

·

可以不阻塞地读普通数据

POLLRDBAND

·

·

可以不阻塞地读优先级数据

POLLPRI

·

·

可以不阻塞地读高优先级数据

POLLOUT

·

·

可以不阻塞地写普通数据

POLLWRNORM

·

·

可以不阻塞地写普通数据

POLLWRBAND

·

·

可以不阻塞地写优先级数据

POLLERR

·

已出错

POLLHUP

·

已挂断

POLLNVAL

·

描述符没有引用一个打开文件

timeout指定超时时间:

timeout == -1: 永远等待。

timeout == 0: 不等待。

timeout > 0: 等待timeout毫秒。

timeout的其他效果与tvptr一样。

函数readv和writev

readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区,有时也将这两个函数称为散步读(scatter read)和聚集写(gather write)。

#include <sys/uio.h>

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

ssize_t write(int fd, const struct iovec* iov, int iovcnt);

返回值:成功,返回已读或已写的字节数;出错,返回-1

说明:

struct iovec

{

void*  iov_base;     /* starting address of buffer */

size_t iov_len;      /* size of buffer */

};

iovcnt指定iov数组中的元素数目,其最大值受限于IOV_MAX。

writev从缓冲区中输出数据的顺序是:iov[0]、iov[1]直至iov[iovcnt - 1],返回输出的字节总数,通常应该等于所有缓冲区长度之和。

Linux I/O 进阶-LMLPHP

readv函数将数据读入缓冲区中,先填满一个缓冲区,再填下一个,返回读到的字节总数,如果遇到文件尾端,返回0。

如下比较了write和writev:将两个缓冲区中的内容写到一个文件中,第一个缓冲区是我们自己创建的,包含第二个缓冲区的长度一个文件中其他信息的文件偏移量;第二个缓冲区是由调用者通过参数传递,有如下3种方式可以实现这一要求:

(1)   调用两次write,每个缓冲区一次。

(2)   分配一个足够大的缓冲区,将两个缓冲区的内容都复制到新缓冲区,然后对这个新缓冲区调用一次write。

(3)   调用writev输出两个缓冲区到文件中。

Linux I/O 进阶-LMLPHP

正如我们预料的,调用两次write的系统时间比调用一次write或writev的长。在缓冲区复制后紧跟一个write所用的CPU时间要少于调用writev所耗费的CPU时间。对于单一的write,我们先将用户层次的两个缓冲区复制到一个分段缓冲区中,然后在调用write时内核将该分段缓冲区中的数据复制到其内部缓冲区中。对于writev,因为内核只需将数据直接复制进其分段缓冲区,所以复制工作应当会少一些。总之,应当用尽量少的系统调用次数来完成任务。如果我们只写少量的数据,将会发现自己复制数据然后调用一次write会比调用writev更合算,但是这也就意味着我们需要管理自己的分段缓冲区,增加了程序的复杂性。

函数readn和writen

通常,在读写一个管道、FIFO以及网络设备及其终端时,需要考虑如下特性:

(1)   一次read操作所返回的数据可能少于要求的数据,即使还没有达到文件尾端也可能是这样,这不是一个错误,应当继续读该设备。

(2)   一次write操作的返回值也可能少于指定输出的字节数,这可能是由某个因素造成的。例如,内核输出缓冲区已满,但这不是错误,所以应当继续写余下的数据。

readn和writen的功能是分别读、写指定的N个字节数据,并处理返回值可能少于要求值的情况,这两个自定义函数按需多次调用read和write,直至读、写了N字节数据。

 /* Read "n" bytes from a descriptor  */
ssize_t readn(int fd, void *ptr, size_t n)
{
size_t nleft;
ssize_t nread; nleft = n;
while (nleft > ) {
if ((nread = read(fd, ptr, nleft)) < ) {
if (nleft == n)
return(-); /* error, return -1 */
else
break; /* error, return amount read so far */
} else if (nread == ) {
break; /* EOF */
}
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
} /* Write "n" bytes to a descriptor */
ssize_t writen(int fd, const void *ptr, size_t n)
{
size_t nleft;
ssize_t nwritten; nleft = n;
while (nleft > ) {
if ((nwritten = write(fd, ptr, nleft)) < ) {
if (nleft == n)
return(-); /* error, return -1 */
else
break; /* error, return amount written so far */
} else if (nwritten == ) {
break;
}
nleft -= nwritten;
ptr += nwritten;
}
return(n - nleft); /* return >= 0 */
} 注:
在已经读、写了一些数据后出错,则这两个函数返回已传输的数据量,而非错误。

存储映射I/O

存储映射I/O将一个磁盘文件映射到存储空间的一个缓冲区上,于是,当从缓冲区中读取数据时,相当于读文件中的相应字节,类似地,将数据写入缓冲区时,相应字节自动地写入了文件中。

为了使用该功能,首先应该告诉内核将一个给定的文件映射到一个存储区域中,由mmap函数可以实现:

#include <sys/mman.h>

void* mmap(void* addr, size_t len, int prot, int flag, int fd, off_t off);

返回值:成功,返回映射区域的起始地址;出错,返回MAP_FAILED

说明:

addr: 指定存储映射区的起始地址,通常将其设为0,表示由系统选择该映射区的起始地址。

fd: 指定要被映射的文件(在文件映射之前,必须先打开该文件)。

len: 要映射的字节数。

off: 要映射的字节在文件中的起始偏移量。

prot参数指定了映射区的保护特性,可以如下取值:

PROT_READ: 映射区可读。

PROT_WRITE: 映射区可写。

PROT_EXEC: 映射区可执行。

PROT_NONE: 映射区不可访问。

注:

对指定映射区的保护说明不能超过文件open模式访问权限(比如,若该文件是只读打开的,那么对该映射区就不能指定PROT_WRITE)。

flag参数如下:

MAP_FIXED: 返回值必须等于addr。这样其实不利于可移植性,因此通常不使用该参数。如果addr非0,那么内核将其视为在何处设置设置映射区的一种建议,不保证会使用所指定的地址;将addr设为0可以获得最大的可移植性。

MAP_SHARED: 此标志指定了操作映射区会同时修改映射文件。

MAP_PRIVATE: 此标志指定了对映射文件创建一个私有副本,对映射区的所有操作不会影响原映射文件。一般可用于调试程序,将程序的正文部分映射至存储区,允许用户修改副本中的指令,而源程序保持不变。MAP_PRIVATE和MAP_SHARED两个参数必须指定一个,且只能指定一个)

映射区位于堆和栈之间,基本如下图所示:

Linux I/O 进阶-LMLPHP

off和addr的值通常要求是系统虚拟页长度的倍数,虚拟页长可使用带参数_SC_PAGESIZE或者_SC_PAGE_SIZE的sysconf函数得到,其实由于off和addr常常指定为0,因此这种要求也不是非常重要。

映射区的长度如果不是页长的整数倍,那么系统将会修改映射区的长度到最近的页长整数倍,但是修改由系统自动加长的映射区部分,不会对源文件造成任何影响。

与映射区有关的信号是SIGSEGV/SIGBUS。如果映射区被mmap指定为只读,但是进程试图写该映射区,那么将产生SIGSEGV信号;如果进程试图访问对其不可用的映射区时,也会产生SIGSEGV信号。如果一个进程访问了另一个进程已经截断的文件区域,那么会产生SIGBUS信号。

子进程通过fork将继承存储映射区(因为子进程复制父进程的地址空间,而存储映射区是该地址空间的一部分),但由于同样的原因,新程序不能通过exec继承存储映射区。

调用mprotect函数可以更改一个现有存储映射区的权限:

#include <sys/mman.h>

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

返回值:若成功,返回0;出错,返回-1

如果共享映射区中的页发生了修改,那么可以调用msync将该页冲刷到映射文件中:

#include <sys/mman.h>

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

返回值:成功,返回0;出错,返回-1

说明:

如果映射区是私有的,那么不修改被映射的文件。flags参数指定了冲刷缓冲区的程度。可以指定MS_ASYNC简单地操作要写回的页。指定MS_SYNC表示在返回之前等待写操作完成。

当进程终止时,会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区:

#include <sys/mman.h>

int munmap(void* addr, size_t len);

返回值:成功,返回0;出错,返回-1

说明:

调用munmap并不会是映射区的内容写回到磁盘文件上。对于MAP_SHARED映射区的文件的更新,会在我们将数据写到映射区后的某个时刻,按内核虚拟存储算法自动执行;而对于MAP_PRIVATE映射区文件的更新会被丢弃掉。

如下程序用于复制文件,类似于cp命令:

 1 [root@benxintuzi IOpri]# ls
deadlock deadlock.c mcopy mcopy.c nonblock nonblock.c templock
3 [root@benxintuzi IOpri]# cat mcopy.c
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h> /*
* * Default file access permissions for new files.
* */
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define COPYINCR (1024*1024*1024) /* 1 GB */ int main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = ; if (argc != )
printf("usage: %s <fromfile> <tofile>\n", argv[]); if ((fdin = open(argv[], O_RDONLY)) < )
printf("can't open %s for reading\n", argv[]); if ((fdout = open(argv[], O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < )
printf("can't creat %s for writing\n", argv[]); if (fstat(fdin, &sbuf) < ) /* need size of input file */
printf("fstat error\n"); if (ftruncate(fdout, sbuf.st_size) < ) /* set output file size */
printf("ftruncate error\n"); while (fsz < sbuf.st_size) {
if ((sbuf.st_size - fsz) > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz; if ((src = mmap(, copysz, PROT_READ, MAP_SHARED,
fdin, fsz)) == MAP_FAILED)
printf("mmap error for input\n");
if ((dst = mmap(, copysz, PROT_READ | PROT_WRITE,
MAP_SHARED, fdout, fsz)) == MAP_FAILED)
printf("mmap error for output\n"); memcpy(dst, src, copysz); /* does the file copy */
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
} return ();
} 61 [root@benxintuzi IOpri]# ./mcopy nonblock copy_nonblock
62 [root@benxintuzi IOpri]# ls
copy_nonblock deadlock.c mcopy.c nonblock.c
deadlock mcopy nonblock templock 说明:
改程序首先打开两个文件,调用fstat获得输入文件的信息(此处主要是长度)。
然后对每个文件调用mmap,将文件映射到内存映射区。
最后调用memcpy将输入缓冲区的内容复制到输出缓冲区中。
为了限制使用内存的量,我们每次最多复制1GB的数据,在映射文件中的后一部分数据之前,我们需  要解除其前一部分数据的映射。

比较:

与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制工作。read和write将数据从内核缓冲区中复制到应用缓冲区中(read),然后再把数据从应用缓冲区复制到另一个内核缓冲区中(write)。

而mmap和memcpy则直接把数据从一个内核缓冲区复制到另一个内核缓冲区,当引用的内存页不存在时,就会发生页错误(缺页中断)。因此,需要根据实际情况,比较一下系统调用和页错误处理的开销,然后决定使用哪种方式能尽可能地提高程序的性能。

05-26 21:33