Apue学习:高级I/O
Posted hjyzjustudy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Apue学习:高级I/O相关的知识,希望对你有一定的参考价值。
无阻塞I/O
nonblocking I/O就是不会阻塞的I/O,就是说我请求一个 I/O操作,然后如果这个I/O操作并不能完成的时候不会阻塞,只是立即返回一个错误代号
指定文件描述符是nonblocking I/O的办法:
1.调用open的时候使用O_NONBLOCKING
2.对于一个已经打开的文件描述符,我们可以使用fcntl来设置O_NONBLOCING属性
记录锁
记录锁,可以锁住文件的一段区域
fcntl函数
int fcntl(int fd, int cmd, struct flock *flocptr);
struct flock
{
short l_type; /* F_RDLCK, F_WR_LCK, F_UNLCK */
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
说明:
可以使用fcntl函数来完成记录锁功能:
cmd参数:
F_GETLK,
F_SETLK,
F_SETLKW
第三个参数是一个指向flock structure 结构的指针
l_type只可以是:
F_RDLCK:共享读锁
F_WRLCK:排他写锁
F_UNLCK:解锁一个区域
指定区域的起始位置由l_start与l_whence共同决定
l_whence:SEEK_SET, SEEK_CUR, SEEK_END
l_start:就是在l_whence的基础上的偏移
l_en:就是要锁定的区域的长度
l_pid:当前获得锁的进程,设置F_GETLK的时候才会设置这个值。这样如果我们的进程想要获得锁,当是如果有其他进程已经获得锁了,那么我们只能被挂起。
注意锁的起始位置与长度可以超过文件的超过文件的结束位置,但是不可以比在文件的开始位置的前面。
注意:
读锁与写锁的控制区域的区别。 注意对于一个进程来说它只能锁住文件的一个区域,它是不能够锁住多个区域。
并且,想要获得读锁,那么文件就必须是读打开,如果想要获得写锁,那么文件就必须是写打开。记录锁与进程以及文件有关,有3个特点:
1.当一个进程关闭了,那么它的锁也被释放,并且如果关闭一个文件描述符,与这个文件描述符有关的文件,这个文件上的所有锁都被释放了。
2.在fork之后,子进程是没有继承父进程的锁的
3.在exec之后,新的程序是继承了原程序的锁的,但是我们可以在文件描述的flags中添加close-on-exec注意在一个文件末尾加锁的问题:
内核会把相对位移转变为一个文件的绝对偏移。这样如果我们在文件的末尾加锁,然后写入,之后解锁文件末尾,这个时候的末尾就不是我们开始的末尾了,因为我们已经写入了一些东西。
这个问题的原因在于我们并不能获得文件的绝对偏移,因为我们在调用lseek之后,别的进程可能又写入的一些东西,造成文件的长度的改变。
cmd参数
F_GETLK:测试我们是不是可以获得锁
F_SETLK:对文件设置我们的锁,但是如果我们的设置的锁与别的进程的锁冲突了,那么fcntl就会立即返回并且把errno设置为EACCES或者EAGAIN,并且清理掉我们的锁。
F_SETLKW:F_SETLK的阻塞版本。也就是如果我们不能够获得锁的话,就会被挂起,知道别的进程释放了这个区间的锁才会唤醒我们。
I/O复用
概述
解决进程要读多个文件的解决办法:
1.生成子进程去读。
2.使用polling技术,在单个进程中,我们可以设置文件描述符为nonblocking,然后依次的给文件发read调用:如果这个文件没有准备后,就会立即返回,因为它是nonblocking,然后再给下一个文件发read,这样循环。
但是这样做造成CPU资源的浪费。
3.使用异步I/O,即我们可以让内核在文件准备好数据之后给我们发送一个信号。
但是异步I/O并不是每个系统都支持,另外也不知道是哪个文件准备好了。(如果使用一个信号对于一个文件的话,信号是远远不够的)。所以为了决定是哪个描述符准备好了,我们依然要将每个文件描述符设置为nonblocking,并且一次询问
I/O复用技术:
我们可以构造一个链表,这个链表记录我们感兴趣的文件描述符,然后调用一个函数,这个函数在我们感兴趣的文件描述符准备好之后才会返回,返回之后会告诉我们哪一个文件描述符准备好了。
select
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict exceptfds,
struct timeval *restrict tvptr);
说明:
1.maxfdp1:指的我们感兴趣的文件描述符的最大值+1。比如我们感兴趣的文件描述符的最大值是n,那么maxfdp1 = n+1。
注意的是一个系统中的最大文件描述符的个数是有限的,可以在
文件描述符集的相关函数
select返回值
select的返回值:
1.-1表示出错
2.0表示没有文件准备好
3.正值,表示有文件准备好。
准备好的含义:
1.读准备好:表示我们调用read在这个文件上不会阻塞
2.写准备好:表示我们调用write在这个文件上不会阻塞
3.意外准备好:表示有一个意外在对应的文件描述符上产生并排队了。
4.对于指向普通文件的文件描述符来说,他总是准备好了read, write and exception conditions
注意:
注意一个文件描述的阻塞与非阻塞flags并不影响select的阻塞情况。比如我们在一个非阻塞的文件描述符上读,但是我们设置select的等待时间为5,那么select就会等待5秒。
pselect
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主要是多了一个sigmask,信号屏蔽字,也就是说pselect会原子性的安装一个信号屏蔽字,并且保存以前的信号屏蔽字。
poll
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
struct pollfd
{
int fd; //需要检查的文件描述符
short events; //对于fd,我们感兴趣的事件
short revents; //返回到的结果,即发生在fd上面的事件,由内核设置
};
通过一个fdarray[]的数组来指定我们感兴趣的文件描述符,这个数组的长度为nfds,等待时间为timeout
events 与 revents的说明:
异步I/O
概述:
异步I/O:主要是指进程可以发起多个I/O操作,而不用阻塞住或等待任何操作完成
同步I/O:指的是我们需要等待i/o操作返回才能进行下一步操作。
aio control block
POSIX通过AIO来完成,这个是AIO control block
一个AIO control block可以控制一个类型的I/O操作
aio_sigevent指定了异步事件,定义异步操作完成时的通知信号或回调函数。内容如下图:
- sigev_notify定义了通知的类型,有3种:
1.SIGEV_NONE:当异步I/O完成时,并不通知进程
2.SIGEV_SIGNAL:当异步操作完成之后,会发一个信号,这个信号的值由sigev_signo指定
3.SIGEV_THREAD:当异步操作完成之后,会执行sigev_notify_function指定的函数,这个函数的sigval值由sigev_value指定。这个函数是在一个分离的线程中执行,
aio API
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
AIO接口函数,read, write。调用并且返回成功之后,操作系统就会帮助我们接受下面的事情。
int aio_sync(int op, struct aiocb *aiocb);
这个函数是为了强制性写到磁盘。
op:
O_DSYNC:相当于fdatasync
O_SYNC:相当于fsync
int aio_error(const struct aiocb *aiocb);
这个函数是为了获得aio_read/write/sync
的结果:
0:表示aio API调用成功返回
-1:表示调用aio_error失败,失败原因写在errno中
EINPROGRESS:表示aio_read/write/sync依然在排队
其他值:表示aio_read/write/sync操作失败的原因
ssize_t aio_return(const struct aiocb *aiocb);
当调用aio_errno返回成功之后,我们可以调用这个函数来获取相关信息。
如果在异步的API成功之前我们调用了这个函数,会出现未定义的结果。
需要注意的是:对于每一个异步I/O操作,我们都只能调用一次aio_return,一旦我们调用了这个函数,操作系统就会释放掉相关的资源(即相关的返回信息)
如果aio_return本身失败会返回-1。其他情况下回返回异步操作的结果信息,例如read就是返回读了多少字节,write就是返回写了多少字节。
int aio_suspend(const struct aiocb *const list[],
int nent,
const struct timespect *timeout);
aio_suspend:假如我们进程想要异步做的事都做完了,只是想要等待I/O操作的完成,那么我们可以调用这个函数。这个
函数会在3中情况下返回:
1.aio_suspend被一个信号打断,这样就会返回-1,并且把errno设置为EINTR
2.如果时间到了,但是没有一个I/O操作准备好,那么就返回-1,并且把errno设置为EAGAIN
3.如果在我们调用这个函数之前,所有我们感兴趣的文件描述符都准备好了,那么就会无阻塞返回
4.如果有一个I/O准备好了,那么就会返回0.
int aio_cancel(int fd, struct aio *aiocb);
这个函数可以取消在fd上进行的异步I/O操作,但是不保证一定可以取消。如果aiocb是NULL,那么就是说我想要取消所有在fd上面的异步I/O操作。
返回值:
AIO_ALLDONE:所有的操作都在取消之前完成了。
AII_CANCELED:所有的取消请求都完成了。
AIO_NOTCANCELED:至少有一个请求没有被取消,aio_cancel失败,失败代号会写入到errno中。
int lio_listio(int mode,
struct aiocb *restrict const list[retrict],
int nent,
struct sigevent *retsrict sigeb);
mode:指示这个I/O是不是真的是异步的。如果是LIO_WAIT,那么lio_listio函数就不会返回直到所有别list指定的I/O操作都完成。
如果是LIO_WAIT,那么这个函数就在所有I/O请求都被排队之后立即返回。进程会在所有被list指定的I/O操作完成之后接受到一个信号,这个信号相关内容由sigev指定。注意在aio control blocks中也会有自己的sigev,所以在单独的一个异步操作完成之后,进程也会受到对应的信号。
aio_lio_opcode指示了这个I/O操作的类型:
LIO_READ:read
LIO_WRITE:write
LIO_NOP:no-op, will be ignored
read:就是说我们相关的AIO control block会被传递到aio_read这个函数中。
readv/writev
ssize_t readv(int fd,
struct iovec *iov, int iovcnt);
ssize_t writev(int fd,
struct iovect *iov, int iovcnt);
struct iovec
{
void *iov_base;
size_t iov_len;
};
readv与writev可以让我们从不连续的buffers中读与写,而且是在一个函数调用中实现。
iov_base:指定了buffer的起始地址
iov_len:指定了buffer的大小
说明:
writev函数依次从iov[0]到iov[iovcnt-1]中收集数据,然后依次写回。
readv函数收集数据到buffers中,注意在填写数据到下一个iov[i]前,会先把iov[i-1]中的buffer数据填写好。
如果readv返回0表示没有数据并且遇到了EOF
readn/writen
pipes,FIFOs以及networks的读写有一些特性:
1.read 函数不一定返回我们需要读的个数,即使我们没有遇到EOF
2.write 函数不一定返回我们需要写的个数。
注意这都不是错误,我们需要重启read 与 write函数,然后继续读写。
但是对于磁盘文件这些就不会发生,如果发生了就说明出现了错误。
ssize_t readn(int fd, void *buf, size_t nbytes);
ssize_t writen(int fd, void *buf, size_t nbytes);
readn与writen都是read与write的多次调用版本。
因为我们指定了我们要读或写多少个字符才会停止,所以readn与writen都会在读/写到指定个数的字符才会返回
I/O映射
概述:
指的是把文件映射到一块内存区域中,这样我们对该内存区域的读写操作就相当于对文件的读写一样了。就省去了read/write系统调用了。
mmap
void *mmap(void *addr, size_t len, int prot,
int flag, int fd, off_t off);
说明:
- addr:表示我们希望映射到的内存的起始位置,NULL表示使用系统我们找到起始位置。
- prot:表示映射区域的保护级别 有4个 PROT_READ: PROT_WRITE: PROT_EXEC: PROT_NON:
- fd:文件描述符,必须先打开这个文件我们才可以映射。
- len:我们希望映射的内容的大小
- off:我们想要的映射的文件的区域的起始地址,off是相对于文件起始位置的偏移。
- flags: MAP_FIXED:指的函数的返回值必须等于addr。这个参数不推荐。
MAP_SHARD:指的是我们对映射区域的操作就相当于我们对文件的操作。 MAP_PRIVATE:对于映射区域的
store操作,会引起对于mapped file的一份私有复制。我们对于映射区域的操作相当于对于这个复制品的操作,原始文件并不会改变。
注意:
- 注意到Linux系统的内存映射单位是一页,所以如果我们的文件大小不够一页,那么也会映射一页的大小,只是多余的内存单元被设置为0,并且对于超出的那一部分的内存单元的操作对于原始文件没有影响,也就是说我们并能够通过mmap来增加文件的大小。
- 与映射有关的信号有两种:
SIGSEGV:表示我们想要接触一块不属于我们的内存区域,如果我们想要对于read-only的区域进行store操作也会引起SIGSEGV信号。
SIGBUS:如果我们想要接触一块已经无效的内存区域就会引起这个信号。比如我们用文件长度映射了一个文件,但是在引用这个内存区域的时候,另一个进程把文件截短了,那么文件长度就变换了。如果我们要对于截取的那部分进行操作就会收到SIGBUS信号。
fork之后的子进程会继承映射的内存区域,但是exec之后就失去了。
mprotect
int mprotec(void *addr, size_t len, int prot);
可以用来改变映射好的内存区域的prot属性。
注意addr有可能需要是page size的整数倍。
msync
int msync(void *addr, size_t len, int flags);
msync:写回
flags参数:可以用来指明我们如何flush内存。
MS_ASYNC:只是说明我们想要写慧。即异步或者延迟写
MS_SYNC:同步写回,说明我们要等到真的写到磁盘才返回。
munmap
int munmap(void *addr, size_t len);
munmap取消内存映射。
munmap并不会影响我们映射好的文件,也就是说,munmap不会使得映射好的内存区域写回到磁盘文件。
写回到磁盘文件只会发生在我们使用MAP_SHARED的时候store了这一内存,或者使用了msync
注意:
- 存储映射I/O做的工作比read/write要小。
read会把内核缓冲区的数据copy到应用程序的缓冲区中。然后write的时候会把应用程序的缓冲区copy到内核缓冲区中。
而存储映射I/O会把内核缓冲区的数据映射到内存中,然后写的时候,比如要写到另一个文件,那么我们只用把这一部分的内存区域store到另一个文件映射到的内存区域就可以了。 - 使用存储映射I/O对于复制普通文件来说比较有效率,但是还是有一些限制,比如我们不可以在一些设备比如网络设备上使用这个技术,并且我们必须注意文件大小的改变问题。
以上是关于Apue学习:高级I/O的主要内容,如果未能解决你的问题,请参考以下文章