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对于复制普通文件来说比较有效率,但是还是有一些限制,比如我们不可以在一些设备比如网络设备上使用这个技术,并且我们必须注意文件大小的改变问题。
时间: 2024-07-30 12:10:44

Apue学习:高级I/O的相关文章

APUE 学习笔记(九) 高级I/O

1. 非阻塞I/O 低速系统调用时可能会使进程永远阻塞的一类系统调用,包括以下调用: (1)某些文件类型你(网络socket套接字.终端设备.管道)暂无可使用数据,则读操作可能会使调用者永远阻塞 (2)如果数据不能立即被(1)中文件类型接受,则写操作会使调用者永远阻塞 (3)某些进程间通信函数 非阻塞I/O使我们可以调用open.read.write这样的I/O操作,并使这些操作不会永远阻塞,如果这种操作不能完成,则调用立即出错返回 对于一个给定的文件有两种方法对其指定非阻塞I/O: (1)调用

APUE 学习笔记(十) 高级I/O

1. Unix IPC(InterProcess Communication) 同一主机的各个进程间的IPC:管道.FIFO.消息队列.信号量.共享存储器 不同主机上的各个进程间IPC:socket套接字 2. 管道 管道进行IPC有两个局限: (1) 半双工,即数据只能在一个方向上流动 (2) 只能在具有公共祖先的进程之间使用.通常,一个管道由一个进程创建,然后该进程调用fork,此后 父子进程之间可以使用该管道 fstat函数对管道的每一端都返回一个FIFO类型的文件描述符,可以用S_ISF

APUE学习笔记:第九章 进程关系

9.1 引言 本章将更详尽地说明进程组以及POSIX.1引入的会话的概念.还将介绍登陆shell(登录时所调用的)和所有从登陆shell启动的进程之间的关系. 9.1 终端登陆 系统管理员创建通常名为/etc/ttys的文件,其中每个终端设备都有一行,每一行说明设备名传递给getty程序的参数.当系统自举时,内核创建进程ID为1的进程,依旧是init进程.init进程使系统进入多用户状态.init进程读文件/etc/ttys,对每一个允许登陆的终端设备,init调用一次fork,所生成的子进程则

php学习高级-提高PHP编程效率的几点建议

1.如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍. 2.$row['id'] 的速度是$row[id]的7倍. 3.echo 比 print 快,并且使用echo的多重参数(译注:指用逗号而不是句点)代替字符串连接,比如echo $str1,$str2. 4.在执行for循环之前确定最大循环数,不要每循环一次都计算最大值,最好运用foreach代替. 5.注销那些不用的变量尤其是大数组,以便释放内存. 6.尽量避免使用__get,__set,__autol

APUE学习笔记:第七章 进程环境

7.1 引言 本章将学习:当执行程序时,其main函数是如何被调用的:命令行参数是如何传送给执行程序的:典型的存储器布局是什么样式:如何分配另外的存储空间:进程如何使用环境变量:各种不同的进程终止方式等:另外还将说明longjmp和setjmp函数以及它们与栈的交互作用:还将介绍研究进程的资源限制 7.2 main函数 C程序总是从main函数开始执行.当内核执行C程序时,在调用main前先调用一个特殊的启动例程.可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编

APUE学习笔记:第四章 文件和目录

4.1 引言 本章将描述文件的特征和文件的性质 4.2 stat.fstat和lstat函数 #include<sys/stat.h> int stat(const char *restrict pathname,struct stat *restrict buf); int fstat(int filedes,struct stat *buf) int lstat(const char *restrict pathname,struct stat *restrict buf); 三个函数的返

APUE 学习笔记(二) 文件I/O

1. 文件I/O 对于内核而言,所有打开的文件都通过文件描述符引用,内核不区分文本文件和二进制文件 open函数:O_RDONLY  O_WRONLY  O_RDWR create函数: close函数:关闭一个文件时还会释放该进程加在该文件上的所有记录锁 lseek函数:显式地为一个打开的文件设置其偏移量 每个打开的文件都有一个与其相关联的 "当前文件偏移量",用以度量从文件开始处计算的字节数,通常,读.写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数 文件偏移量可以大于

APUE 学习笔记(一) Unix基础知识

1. Unix 体系结构 内核的接口被称为系统调用 公用函数库构建在系统调用接口之上 应用软件既可以调用公用函数库,也可以直接进行系统调用 2. 文件和目录 目录操作函数:opendir---> readdir---> closedir struct dirent 结构体 stat 系统调用 3.程序.进程.线程 程序:存放在磁盘上.并处于某个目录中的一个可执行文件.使用exec系列函数将程序从磁盘读入存储器,并使其执行 进程:程序的执行实体.进程控制的3个函数:fork.exec.waitp

(十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO

. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录 (四) 一起学 Unix 环境高级编程 (APUE) 之 系统数据文件和信息 (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境 (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制 (七) 一起学 Unix 环境高级编程 (APUE)

APUE学习笔记:第一章 UNUX基础知识

1.2 UNIX体系结构 从严格意义上,可将操作系统定义为一种软件(内核),它控制计算机硬件资源,提供程序运行环境.内核的接口被称为系统调用.公用函数库构建在系统调用接口之上,应用软件即可使用公用函数库,也可使用系统调用.shell是一种特殊的应用程序,它为运行其他应用程序提供了一个接口 从广义上,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并给予计算机以独有的特性(软件包括系统实用程序,应用软件,shell以及公用函数库等) 1.3  shell shell是一个命令行解