I/O
使我们可以发出open
、read
和write
这样的I/O
操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如果继续进行将阻塞。read
返回-1
,errno
为EAGAIN
。I/O
指的是文件状态标志,即与文件表项有关。会影响使用同一文件表项的所有文件描述符(即使属于不同的进程)。read
函数阻塞的情况:read
函数只是一个通用的读文件设备的接口。是否阻塞需要由设备的属性和设定所决定。一般来说,读字符终端、网络的socket
描述字,管道文件等,这些文件的缺省read
都是阻塞的方式。如果是读磁盘上的文件,一般不会是阻塞方式的。但使用锁和fcntl
设置取消文件O_NOBLOCK
状态,也会产生阻塞的read
效果。I/O
:
open
获得描述符,指定O_NONBLOCK
标志fcntl
,由该函数打开O_NONBLOCK
文件状态标志int flag = fcntl(fd, F_GETFL); //获取文件状态标志
flag |= O_NONBLOCK;
int ret = fcntl(fd, F_SETFL, flag); //设置文件状态标志
I/O
的实例。它从标准输入读50000
个字节,并试图将它们写到标准输出上。该程序先将标准输出设置为非阻塞的,然后用for
循环进行输出,每次write
调用的结果都在标准错误上打印。其中set_fl
函数的介绍见3.14
节,get_fl
函数则类似于set_fl
函数。#include "apue.h"
#include
#include
char buf[500000];
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 > 0) {
errno = 0;
nwrite = write(STDOUT_FILENO, ptr, ntowrite);
fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);
if (nwrite > 0) {
ptr += nwrite;
ntowrite -= nwrite;
}
}
clr_fl(STDOUT_FILENO, O_NONBLOCK); /* clear nonblocking */
exit(0);
}
若标准输出是普通文件,则write
一般只调用一次。这里的文件/etc/services
大小为19605
字节,小于程序中的50000
字节,所以写入的大小也为19605
字节。如果文件大小大于50000
字节,那么写入的大小为50000
字节。
lh@LH_LINUX:~/桌面/apue.3e/advio$ ls -l /etc/services
-rw-r--r-- 1 root root 19605 10月 25 2014 /etc/services
lh@LH_LINUX:~/桌面/apue.3e/advio$ ./nonblockw < /etc/services > temp.file
read 19605 bytes
nwrite = 19605, errno = 0
lh@LH_LINUX:~/桌面/apue.3e/advio$ ls -l temp.file
-rw-rw-r-- 1 lh lh 19605 8月 9 13:15 temp.file
若标准输出是终端,则有时返回数字,有时返回错误,下面是运行结果。
errno
的值35
对应的是EAGAIN
。终端驱动程序一次能接受的数据量随系统而变。9000
多个write
调用,但是只有500
个真正输出了数据,其余的都只返回了错误。这种形式的循环称为轮询,在多用户系统上用它会浪费CPU时间。14.4
节会介绍非阻塞描述符的I/O
多路转换,这是进行这种操作的一种比较有效的方法。I/O
。如若我们能在其他线程中继续进行,则可以允许单个线程在I/O
调用中阻塞。但线程间的同步的开销有时却可能增加复杂性,于是导致得不偿失的后果。fcntl
记录锁3.14
节已经给出了该函数的原型
int fcntl(int fd, int cmd, ... /* struct flock * flockptr */ );
cmd
是F_GETLK
、F_SETLK
、F_SETLKW
。
F_GETLK
:
flockptr
描述的锁是否会被另外一把锁排斥(阻塞)。如果存在一把锁阻止创建flockptr
描述的锁,则该现有锁的信息将重写flockptr
指向的信息。如果不存在这种情况,除了l_type
设置为F_UNLCK
之外,flockptr
指向的结构中其他信息不变。F_GETLK
不会报告调用进程自己持有的锁信息。因此不能用它来测试自己是否在某一文件区域持有一把锁。F_SETLK
:
flockptr
所描述的锁(共享读锁或独占写锁)。如果失败fcntl
函数立即出错返回,errno
设置为EACCES
或EAGAGIN
F_SETLKW
:
F_SETLK
的阻塞版本(w
表示等待wait
)。如果所请求的读锁或写锁因另一个进程当前已经对所请求部分进行了加锁而不能被授予,那么调用进程休眠。如果请求创建的锁已经可用,或者休眠被信号中断,则该进程被唤醒。flock
结构的指针。struct flock
{
short int l_type; /* 记录锁类型: F_RDLCK, F_WRLCK, or F_UNLCK. */
short int l_whence; /* SEEK_SET、SEEK_CUR、SEEK_END */
__off_t l_start; /* Offset where the lock begins. */
__off_t l_len; /* Size of the locked area; zero means until EOF. */
__pid_t l_pid; /* Process holding the lock. */
};
flock
结构说明如下:
l_type
:所希望的锁类型。F_RDLCK
(共享读锁)、F_WRLCK
(独占性写锁)、F_UNLCK
(解锁一个区域)l_whence
:指示l_start
从哪里开始。SEEK_SET
(开头)、SEEK_CUR
(当前位置)、SEEK_END
(结尾)l_start
:要加锁或解锁区域的起始字节偏移量l_len
:要加锁或解锁区域字节长度l_pid
:仅由F_GETLK
返回,表示该pid
进程持有的锁能阻塞当前进程。l_len
为0
,则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向该文件中追加写了多少数据,它们都可以处于锁的范围内(不必猜测会有多少字节被追加写到了文件之后)l_start
和l_whence
指向文件起始位置,并且指定长度l_len
为0
。fcntl
可以操作两种锁:共享读锁F_RDLCK
和独占性写锁F_WRLCK
需要注意以下两点:
F_GETLK
测试能否建立一把锁,然后用F_SETLK
或F_SETLKW
企图建立那把锁,这两者不是一个原子操作。不能保证两次fcntl
调用之间不会有另一个进程插入并建立一把锁POSIX
没有说明下列情况会发生什么:
文件记录锁的组合和分裂
实例:请求和释放一把锁。为了每次都避免分配flock
结构,然后又填入各项信息,可以用下图程序中的lock_reg
来处理这些细节。
实例:测试一把锁。如果存在一把锁,它阻塞由参数指定的锁请求,则此函数返回持有这把现有锁的进程的进程 ID
,否则此函数返回0
。
实例:死锁。该例中子进程对第0
字节加锁,父进程对第1
字节加锁。然后,它们中的每一个又试图对对方已经加锁的字节加锁。 程序中介绍了8.9节中介绍的父进程和子进程同步例程。
#include "apue.h"
#include
static void
lockabyte(const char *name, int fd, off_t offset)
{
if (writew_lock(fd, offset, SEEK_SET, 1) < 0)
err_sys("%s: writew_lock error", 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)) < 0)
err_sys("creat error");
if (write(fd, "ab", 2) != 2)
err_sys("write error");
TELL_WAIT(); /*set things up for TELL_xxx & WAIT_xxx */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
lockabyte("child", fd, 0);
TELL_PARENT(getppid()); /*tell parent we're done */
WAIT_PARENT(); /*and wait for parent*/
lockabyte("child", fd, 1);
} else { /* parent */
lockabyte("parent", fd, 1);
TELL_CHILD(pid); /*tell child we're done */
WAIT_CHILD(); /*and wait for parent*/
lockabyte("parent", fd, 0);
}
exit(0);
}
运行该实例可以得到
lh@LH_LINUX:~/桌面/apue.3e/advio$ ./deadlock
parent: got the lock, byte 1
child: got the lock, byte 0
parent: writew_lock error: Resource deadlock avoided
child: got the lock, byte 1
close(fd)
后,在fd1
设置的锁被释放。dup
函数的使用方法见3.12
节fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);
close(fd2);
dup
替换成open
,其效果也一样:fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = open(fd1);
close(fd2);
fork
产生的子进程不继承父进程所设置的锁。因为对于父进程获得的锁而言,子进程被视为另一个进程。exec
后,新程序可以继承原执行程序的锁。但是如果该文件描述符设置了close-on-exec
标志,则exec
之后释放相应文件的锁。fd1 = open(pathname,...);
write_lock(fd1,0,SEEK_SET,1); // 该函数是自定义的,父进程在字节0上设置写锁
if((pid = fork()) > 0) { // 父进程
fd2 = dup(f1);
fd3 = open(pathname,...);
} else if(pid == 0) { // 子进程
read_lock(fd1,1,SEEK_SET,1); //该函数是自定义的,子进程在字节1上设置读锁
}
pause();
在父进程和子进程暂停(执行pause()
)之后,数据结构的情况如下v节点
/inode节点
上的(而不是在文件表项中的),其实现是通过一个链表记录该文件上的各个锁,因此能保证多个进程正确操作文件记录锁。在图中显示了两个lockf
结构,一个是由父进程调用write_lock
形成的,另一个则是子进程调用read_lock
形成的。每一个结构都包含了相应的进程ID
。fd1
、fd2
、fd3
中的任意一个都将释放由父进程设置的写锁。内核会从该描述符锁关联的inode
节点开始,逐个检查lockf
链表中的各项,并释放由调用进程持有的各把锁。lockfile
函数实现如下:守护进程可用该函数在文件整体上加独占写锁。int lockfile(int fd) {
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_start = 0;
fl.l_whence = SEEK_SET;
fl.l_len = 0;
return fcntl(fd,F_SETLK,&fl);
}
另一种方法是write_lock
函数定义lockfile
函数:#define lockfile(fd) write_lock((fd),0,SEEK_SET,0)
write_lock(fd,0,SEEK_END,0);
write(fd,buf,1);
un_lock(fd,0,SEEK_END);
write(fd,buf,1);
API
,比如fcntl
记录锁F_GETTLK
来主动检查是否有锁存在。如果有锁存在并被排斥,那么就主动保证不再进行接下来的I/O
操作。如果每一个进程都主动进行检查,并主动保证,那么就说这些进程都以一致性的方法处理锁。Linux
默认是采用建议性锁)对一个特定文件打开其设置组ID
位、关闭其组执行位便开启了对该文件的强制性锁机制。因为当组执行位关闭时,设置组ID
位不再有意义,因此定义以这两者组合来指定该文件的锁是强制性的而不是建议性的。
强制性锁机制是这样规定的:建议性锁中提到的破坏性I/O
操作可能会被内核禁止(即在该文件范围存在锁时强行进行I/O
操作)。强制性锁会让内核检查每一个open
、read
、write
操作,如果这些操作违背了该文件上的某一把锁,则I/O
操作会被禁止(阻塞或直接出错返回)。也就是强制性锁机制,让锁变得名副其实,真正达到了锁的效果,而不是像建议性锁机制那样只是个纸老虎。
如下图所示,当一个进程试图read
、write
一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了锁,那么结果取决于三个方面:
注意,对于open
函数,即使正在打开的文件具有强制性记录锁,该open
也会成功。随后的read
或write
依从与上图所示规则。除非open
以O_TRUNC
打开该文件,那么open
出错返回,因为如果另一个进程拥有它的读锁或写锁,那么该文件就不能被截断为0
。
回到最初的问题,如果两个人同时编辑同一个文件时将会怎样?一般的UNIX文本编辑器不使用记录锁,因此该文件的最后结果取决于写该文件的最后一个进程。但是如果该UNIX文本编辑器支持记录锁则不会是这样,具体结果会根据上面具体介绍的内容。
#include "apue.h"
#include
#include
#include
int
main(int argc, char *argv[])
{
int fd;
pid_t pid;
char buf[5];
struct stat statbuf;
if (argc != 2) {
fprintf(stderr, "usage: %s filename\n", argv[0]);
exit(1);
}
if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
err_sys("open error");
if (write(fd, "abcdef", 6) != 6)
err_sys("write error");
/* turn on set-group-ID and turn off group-execute */
if (fstat(fd, &statbuf) < 0)
err_sys("fstat error");
if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
err_sys("fchmod error");
TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
/* write lock entire file */
if (write_lock(fd, 0, SEEK_SET, 0) < 0)
err_sys("write_lock error");
TELL_CHILD(pid);
if (waitpid(pid, NULL, 0) < 0)
err_sys("waitpid error");
} else { /* child */
WAIT_PARENT(); /* wait for parent to set lock */
set_fl(fd, O_NONBLOCK);
/* first let's see what error we get if region is locked */
if (read_lock(fd, 0, SEEK_SET, 0) != -1) /* no wait */
err_sys("child: read_lock succeeded");
printf("read_lock of already-locked region returns %d\n",
errno);
/* now try to read the mandatory locked file */
if (lseek(fd, 0, SEEK_SET) == -1)
err_sys("lseek error");
if (read(fd, buf, 2) < 0)
err_ret("read failed (mandatory locking works)");
else
printf("read OK (no mandatory locking), buf = %2.2s\n",
buf);
}
exit(0);
}
EAGAIN
或EACESS
。接着,子进程将文件读、写位置调整到文件起点,并试图读(read
)该文件。如果系统提供强制性锁,则read
应返回EAGAIN
或EACESS
(因为该描述符是非阻塞的),否则read
返回所读的数据。I/O
while((n = read(STDIN_FILENO,buf,BUFSIZ) > 0) {
if(write(STDOUT_FILENO,buf,n) != n)
err_sys("write error");
}
I/O
多路转接技术:先构造一张我们感兴趣的描述符列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O
时,该函数才返回。poll
、pselect
、select
、epoll
函数可以使我们能够进行I/O
多路转接。这些函数返回时,进程告诉我们哪些文件描述符已经准备好可以进行I/O
select
和pselect
select
可以进行I/O
多路转接。传给select
的参数告诉内核:
而select
返回时告诉我们:
使用这些返回信息,就可调用相应的I/O
函数(一般是read
和write
),并且确知该函数不会阻塞。
int select(int nfds,
fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
struct timeval *timeout);
//返回准备就绪的描述符数目
nfds
:
readfds
/writefds
/exceptfds
fd_set
,我们它只是一个很大的字节数组。fd_set
描述符集合类型,可以使用以下函数进行操作:void FD_SET(int fd,fd_set* fdsetp); // 开启描述符集中的一位
void FD_CLR(int fd,fd_set* fdsetp); // 清除描述符集中的一位
int FD_ISSET(int fd,fd_set* fdsetp); // 测试描述符集中一位是否打开
void FD_ZERO(fd_set* fdsetp); // 将描述符集中所有位置0
fd_set
对象后,一定要使用FD_ZERO
将这个描述符集置0
。select
函数返回时,这三个描述符集将包含了所有满足条件的描述符,此时可以用FD_ISSET
进行测试。timeout
:
timeout == NULL
:永远等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,返回-1
,errno
设为EINTR
。timeout->tv_sec == 0 && timeout->tv_usec == 0
:不等待,测试指定的描述符并立即返回timeout->tv_sec!=0 || timeout->tv_usec!=0
:等待指定时间。当指定的描述符之一准备好,或指定时间超时时返回。如果超时时还没有一个描述符准备好则返回0
。如果捕捉到一个信号则被中断返回-1
。若在等待时间尚未到期时select
返回,那么将用剩余时间值更新该结构。-1
:出错(如捕捉到信号中断)。此时三个描述符集都不修改0
:超时后没有描述符准备好。此时三个描述符集都置0
>0
:已经准备好的描述符数。此数字是三个描述符集中已准备好的描述符数目之和,所以如果同一描述符已准备好读和写,则返回值中对其计两次数。三个描述符集中仍然打开的位对应于已准备好的描述符。read
操作不会阻塞 ,则认为此描述符是读准备好的。write
操作不会阻塞 ,则认为此描述符是写准备好的。select
认为该描述符可读,然后调用read
返回0
,这是UNUX系统指示到达文件尾端的方法。pselect
是select
函数的变体
int pselect(int nfds,
fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
const struct timespec *timeout,const sigset_t *sigmask);
pselect
和select
相同
timespec
结构,提供纳秒级别精度const
类型,若未超时就返回,pselect
并不会改变此值。sigmask
参数为NULL
则和select
相同;否则sigmask
指向一信号屏蔽字,在调用pselect
时,安装该信号屏蔽字,函数返回前恢复以前的信号屏蔽字。aiocb
结构定义了AIO控制块,它至少包括以下字段:/* Asynchronous I/O control block. */
struct aiocb
{
int aio_fildes; /* 被打开用来读写的文件描述符 */
off_t aio_offset; /* 读写操作从该偏移量开始 */
int aio_lio_opcode; /* 仅被 lio_listio() 函数使用 */
int aio_reqprio; /* 请求优先级 */
volatile void * aio_buf; /* 对于读操作,数据复制到该缓冲区;对于写操作,数据从该缓冲区中复制出来 */
size_t aio_nbytes; /* 要读写的字节数 */
struct sigevent aio_sigevent; /* I/O事件完成后如何通知程序 */
};
I/O
操作必须显式的指定偏移量aio_offset
,因为异步I/O接口并不影响(或使用)由操作系统维护的文件表项中记录的偏移量。I/O
向一个以追加模式(O_APPEND
)open
的文件中写入数据,则aio_offset
字段被忽略。aio_reqprio
字段是异步I/O请求提示顺序(请求优先级)。但是系统对于该顺序只有有限的控制力,即不一定遵循。aio_lio_opcode
字段只能用于基于列表的异步I/O(仅被 lio_listio()
函数使用)aio_sigevent
字段控制I/O事件完成后,如何通知应用程序,通过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
:异步I/O
请求完成后,不通知进程SIGEV_SIGNAL
:异步I/O
请求完成后,产生由sigev_signo
字段指定的信号。如果应用程序要捕获该信号,且在sigaction
时指定了SA_SIGINFO
标志,那么该信号将被入队(如果支持排队信号)。信号处理程序sa_sigaction
中第二个参数siginfo_t
中的si_value
字段值被置为sigev_value
。SIGEV_THREAD
:当异步I/O
请求完成时,调用sigev_notify_function
函数。该函数的sigval
参数值为sigev_value
。系统会自动将该函数在分离状态下的一个单独的线程中执行(该线程的属性是sigev_notify_attributes
)。aio_read
和aio_write
函数
aiocb
结构体后,就可以调用aio_read
函数来进行异步读操作,或调用aio_write
函数来进行异步写操作。/*对 aiocbp 描述的异步I/O 请求进行排队。这个函数是 read(2) 的异步模拟。*/
int aio_read(struct aiocb *aiocbp);
/*对由 aiocbp 描述的异步 I/O 请求进行排队。此函数是 write(2) 的异步模拟*/
int aio_write(struct aiocb *aiocbp);
I/O
请求便已经被操作系统放入等待处理的队列中了。I/O
操作的结果没有任何关系。I/O
操作在等待时,必须保证aiocb
对象中的缓冲区等资源始终是可用的。aio_fsync
函数
AIO
控制块并调用 aio_fsync
函数。aio_read
和aio_write
一样,该函数只是一个请求(即一个同步请求),它并不等待I/O
结束(只是将同步请求入队),而是立即返回int aio_fsync(int op, struct aiocb *aiocbp);
aiocbp
参数
aio_fildes
字段指定了要同步的异步写操作的文件。aiocbp
指向的结构体成员 aio_fildes
之外唯一被本函数使用成员是 aio_sigevent
,这个成员指出在操作完成时希望收到哪种异步通知。所有其它字段都被忽略。op
参数
O_DSYNC
:所有当前在队的异步 I/O
操作好比调用了 fdatasync(2)
一样将会完成O_SYNC
:所有当前在队的 异步I/O
操作好比调用了 fsync(2)
一样将会完成aio_read
和aio_write
一样,在安排了同步后(即同步请求成功排队),aio_fsync
函数立即返回。如果返回0
说明成功,失败返回-1
并置位errno
。aio_error
函数
aio_error
函数int aio_error(const struct aiocb *aiocbp);
0
:异步操作成功完成,需要调用aio_return
函数获取操作返回值-1
:失败,并置位errno
EINPROGRESS
:异步读、写或同步操作仍在等待ECANCELED
代表该异步I/O
被取消了aio_return
函数
aio_return
函数来获取异步操作的返回值ssize_t aio_return(struct aiocb *aiocbp);
-1
:调用失败,置位errno
read
、write
或者fsync
在被成功调用时可能返回的结果aio_suspend
函数
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
操作已经完成,则不阻塞直接返回aio_cancel
函数
I/O
操作时,可以尝试用aio_cancel
函数取消它们。注意,系统无法保证能够取消该异步I/O操作,所以是尝试。int aio_cancel(int fd, struct aiocb *aiocbp);
fd
:未完成的异步I/O
操作的文件描述符aiocbp
参数:如果为NULL
,系统会尝试取消该文件的所有未完成异步I/O
操作;否则,系统会尝试取消由aiocbp
控制块描述的单个异步I/O
操作。AIO_ALLDONE
:所有操作在尝试取消它们之前已经完成AIO_CANCELED
:所有要求的操作已被取消AIO_NOTCANCELED
:至少一个要求的操作没有被取消-1
:调用失败,置位errno
I/O
成功被取消,对相应的AIO
控制块调用aio_error
会返回错误ECANCELED
。lio_listio
函数
aiocb
控制块列表描述的I/O
请求。注意该函数可以指定是异步还是同步。int lio_listio(int mode, struct aiocb *const aiocb_list[],
int nitems, struct sigevent *sevp);
mode
:决定I/O
是否真的是异步的
LIO_WAIT
:同步I/O,即该函数在所有由列表指定的I/O操作完成后返回。此时sevp参数被忽略LIO_NOWAIT
:异步I/O。该函数在I/O请求入队后立即返回。进程将在所有I/O操作完成后,按照sevp参数指定的被异步的通知。sevp
参数
I/O
完成后,按照该参数进行通知。需要与aiocb_list
数组中每个aiocb
的aio_sigevent
字段区分开来,每个元素的aio_sigevent
字段是在该异步I/O
完成后进行通知的,而sevp
参数针对的是所有异步I/O
完成之后。aiocb_list
参数和nitems
参数
I/O
操作。(aiocb_list
是aiocb
数组,nitems
是数组元素个数)aiocb
中的aio_lio_opcode
只在该函数中使用,aio_lio_opcode
字段值:
LIO_READ
:读操作,按照对应的aiocb
控制块传给aio_read
处理LIO_WRITE
:写操作,按照对应的aiocb
控制块传给aio_write
处理LIO_NOP
:将被忽略的空操作#include "apue.h"
#include
#include
#define BSZ 4096
unsigned char buf[BSZ];
unsigned char
translate(unsigned char c)
{
if (isalpha(c)) {
if (c >= 'n')
c -= 13;
else if (c >= 'a')
c += 13;
else if (c >= 'N')
c -= 13;
else
c += 13;
}
return(c);
}
int
main(int argc, char* argv[])
{
int ifd, ofd, i, n, nw;
if (argc != 3)
err_quit("usage: rot13 infile outfile");
if ((ifd = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s", argv[1]);
if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
err_sys("can't create %s", argv[2]);
while ((n = read(ifd, buf, BSZ)) > 0) {
for (i = 0; i < n; i++)
buf[i] = translate(buf[i]);
if ((nw = write(ofd, buf, n)) != n) {
if (nw < 0)
err_sys("write failed");
else
err_quit("short write (%d/%d)", nw, n);
}
}
fsync(ofd);
exit(0);
}
#include "apue.h"
#include
#include
#include
#include
#define BSZ 4096
#define NBUF 8
enum rwop {
UNUSED = 0,
READ_PENDING = 1,
WRITE_PENDING = 2
};
struct buf {
enum rwop op;
int last;
struct aiocb aiocb;
unsigned char data[BSZ];
};
struct buf bufs[NBUF];
unsigned char
translate(unsigned char c)
{
/* same as before */
if (isalpha(c)) {
if (c >= 'n')
c -= 13;
else if (c >= 'a')
c += 13;
else if (c >= 'N')
c -= 13;
else
c += 13;
}
return(c);
}
int
main(int argc, char* argv[])
{
int ifd, ofd, i, j, n, err, numop;
struct stat sbuf;
const struct aiocb *aiolist[NBUF];
off_t off = 0;
if (argc != 3)
err_quit("usage: rot13 infile outfile");
if ((ifd = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s", argv[1]);
if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
err_sys("can't create %s", argv[2]);
if (fstat(ifd, &sbuf) < 0)
err_sys("fstat failed");
/* initialize the buffers */
for (i = 0; i < NBUF; i++) {
bufs[i].op = UNUSED;
bufs[i].aiocb.aio_buf = bufs[i].data;
bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;
aiolist[i] = NULL;
}
numop = 0;
for (;;) {
for (i = 0; i < NBUF; i++) {
switch (bufs[i].op) {
case UNUSED:
/*
* Read from the input file if more data
* remains unread.
*/
if (off < sbuf.st_size) {
bufs[i].op = READ_PENDING;
bufs[i].aiocb.aio_fildes = ifd;
bufs[i].aiocb.aio_offset = off;
off += BSZ;
if (off >= sbuf.st_size)
bufs[i].last = 1;
bufs[i].aiocb.aio_nbytes = BSZ;
if (aio_read(&bufs[i].aiocb) < 0)
err_sys("aio_read failed");
aiolist[i] = &bufs[i].aiocb;
numop++;
}
break;
case READ_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "read failed");
}
/*
* A read is complete; translate the buffer
* and write it.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != BSZ && !bufs[i].last)
err_quit("short read (%d/%d)", n, BSZ);
for (j = 0; j < n; j++)
bufs[i].data[j] = translate(bufs[i].data[j]);
bufs[i].op = WRITE_PENDING;
bufs[i].aiocb.aio_fildes = ofd;
bufs[i].aiocb.aio_nbytes = n;
if (aio_write(&bufs[i].aiocb) < 0)
err_sys("aio_write failed");
/* retain our spot in aiolist */
break;
case WRITE_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "write failed");
}
/*
* A write is complete; mark the buffer as unused.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != bufs[i].aiocb.aio_nbytes)
err_quit("short write (%d/%d)", n, BSZ);
aiolist[i] = NULL;
bufs[i].op = UNUSED;
numop--;
break;
}
}
if (numop == 0) {
if (off >= sbuf.st_size)
break;
} else {
if (aio_suspend(aiolist, NBUF, NULL) < 0)
err_sys("aio_suspend failed");
}
}
bufs[0].aiocb.aio_fildes = ofd;
if (aio_fsync(O_SYNC, &bufs[0].aiocb) < 0)
err_sys("aio_fsync failed");
exit(0);
}
read()
一次将它们读至一个较大的缓冲区中,然后将它们分成若干部分复制到不同的区域; ②调用read()
若干次分批将它们读至不同区域。同样,如果想将程序中不同区域的数据块连续地写至文件,也必须进行类似的处理。UNIX
提供了另外两个函数——readv()
和writev()
,它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* 缓冲区地址 */
size_t iov_len; /* 缓冲区大小 */
};
int main()
{
char buf1[8] = { 0 };
char buf2[8] = { 0 };
struct iovec iov[2];
ssize_t nread;
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);
nread = readv(STDIN_FILENO, iov, 2);
printf("%ld bytes read.\n", nread);
printf("buf1: %s\n", buf1);
printf("buf2: %s\n", buf2);
exit(EXIT_SUCCESS);
}
writev
将多个数据存储在一起,将驻留在两个或更多的不连接的缓冲区中的数据一次写出去。writev
以顺序iov[0]
、iov[1]
至iov[iovcnt-1]
从各缓冲区中聚集输出数据到fd
。ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* 缓冲区地址 */
size_t iov_len; /* 缓冲区大小 */
};
fd
:要写入的文件描述符iov
:iovec
结构体数组,每个结构体代表一个缓冲区iovcnt
:第二个参数数组元素个数-1
并设置相应的errno
。int main()
{
char *str0 = "hello ";
char *str1 = "world\n";
struct iovec iov[2];
ssize_t nwritten;
iov[0].iov_base = str0;
iov[0].iov_len = strlen(str0) + 1;
iov[1].iov_base = str1;
iov[1].iov_len = strlen(str1) + 1;
nwritten = writev(STDOUT_FILENO, iov, 2);
printf("%ld bytes written.\n", nwritten);
exit(EXIT_SUCCESS);
}
ssize_t /* Read "n" bytes from a descriptor */
readn(int fd, void *ptr, size_t n)
{
size_t nleft;
ssize_t nread;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (nleft == n)
return(-1); /* error, return -1 */
else
break; /* error, return amount read so far */
} else if (nread == 0) {
break; /* EOF */
}
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
ssize_t /* Write "n" bytes to a descriptor */
writen(int fd, const void *ptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
nleft = n;
while (nleft > 0) {
if ((nwritten = write(fd, ptr, nleft)) < 0) {
if (nleft == n)
return(-1); /* error, return -1 */
else
break; /* error, return amount written so far */
} else if (nwritten == 0) {
break;
}
nleft -= nwritten;
ptr += nwritten;
}
return(n - nleft); /* return >= 0 */
}
I/O
能将一个磁盘文件映射到存储空间中的一个缓冲区上。于是当从缓冲区中取数据时,就相当于读文件中的相应字节;将数据存入缓冲区时,相应字节就自动写入文件。就可以在不使用read
和write
的情况下执行I/O
。mmap
是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。 mmap()
系统调用使得进程之间通过映射同一个普通文件实现共享内存。mmap
函数告诉内核将一个给定的文件映射到一个存储区域中void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
//成功返回映射区起始地址;失败返回MAP_FAILED
addr
:
length
:
stat
系统调用获得打开文件的大小信息,然后设置为这个参数prot
:
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
文件内容偏移量)mmap
函数需要注意以下几点:
addr
和offset
的值通常被要求是系统虚拟存储页长度的倍数(可以通过sysconf(_SC_PAGESIZE)
得到)。但是addr和offset常被设置为0,因此该要求不重要。SIGSEGV
和SIGBUS
。fork
后子进程继承存储映射区exec
之后的新程序不继承存储映射区int mprotect(void *addr, size_t len, int prot);
prot
参数值与mmap
的prot
参数一样。
PROT_EXEC
映射区可以被执行PROT_READ
映射区可以被读取PROT_WRITE
映射区可以被写入PROT_NONE
映射区不可访问addr
必须是系统页长整数倍MAP_SHARED
标志映射到地址空间的,那么修改并不会立即写回到文件中。何时写回脏页面由内核的守护进程决定。MAP_SHARED
),那么可以调用msync
将该页冲洗到被映射的文件中。msync
类似于fsync
,但作用于存储映射区。如果映射是私有的(MAP_PRIVATE
),那么不修改被映射的文件。int msync(void *addr, size_t length, int flags);
flags
参数:
MS_ASYNC
:异步。调用会立即返回,不等到更新的完成;MS_SYNC
:同步。在该函数返回之前等待写操作完成。如果要确保数据安全的写到了文件中,则需要在进程终止前以MS_SYNC
标志调用msync
函数munmap
函数解除映射区。注意,close
映射存储区时使用的文件描述符并不解除映射区。int munmap(void *addr, size_t length);
munmap
函数并不会使映射区的内容写到磁盘文件上。MAP_SHARED
映射的磁盘文件的更新,会在我们将数据写到存储映射区后的某个时刻,按照内存虚拟存储算法自动进行。MAP_PRIVATE
存储区的修改会被丢弃。#include "apue.h"
#include
#include
#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 = 0;
if (argc != 3)
err_quit("usage: %s " , argv[0]);
if ((fdin = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s for reading", argv[1]);
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < 0)
err_sys("can't creat %s for writing", argv[2]);
if (fstat(fdin, &sbuf) < 0) /* need size of input file */
err_sys("fstat error");
if (ftruncate(fdout, sbuf.st_size) < 0) /* set output file size */
err_sys("ftruncate error");
while (fsz < sbuf.st_size) {
if ((sbuf.st_size - fsz) > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz;
if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED,
fdin, fsz)) == MAP_FAILED)
err_sys("mmap error for input");
if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,
MAP_SHARED, fdout, fsz)) == MAP_FAILED)
err_sys("mmap error for output");
memcpy(dst, src, copysz); /* does the file copy */
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
}
exit(0);
}