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的主要内容,如果未能解决你的问题,请参考以下文章

apue 第14章 高级I/O

APUE---标准I/O库

[03]APUE:文件 I/O

APUE---文件I/O

APUE:文件I/O

APUE---文件和目录