linux学习笔记--高级I/O

阻塞IO:对文件的IO操作是哦阻塞的,非阻塞IO就是操作时相反的,非阻塞的。

在使用open()函数打开文件时,参数flags指定O_NOONBLOCK标志时,后续IO操作就是非阻塞方式进行,如果未指定该标志,就默认以阻塞式操作。当然对于普通文件的读写操作是不会阻塞,所有指定该标志没有影响的。

I/O多路复用

通过一种机制,可以监视多个文件描述符。是一种并发式的非阻塞I/O。

selsect()函数

系统调用该函数可用于执行I/O多路复用操作,调用该函数会一直阻塞,直到某一个或多个文件描述符成为就绪态。函数原型如下:

#include<sys/select.h>

int select(int nfds,fd_set *readfds,fd_set  *writefds,fd_set *exceptfds,struct timeval *timeout);

参数 readfds、writefds 以及 exceptfds 都是 fd_set 类型指针, 指向一个 fd_set 类型对象,fd_set 数据类型是一个文件描述符的集合体,所以参数 readfds、writefds 以及 exceptfds 都是指向文件描述符集合的指针,这些参数按照如下方式使用:

⚫ readfds 是用来检测读是否就绪(是否可读)的文件描述符集合;

⚫ writefds 是用来检测写是否就绪(是否可写)的文件描述符集合;

⚫ exceptfds 是用来检测异常情况是否发生的文件描述符集合。

如果对 readfds、writefds 以及 exceptfds 中的某些事件不感兴趣,可将其设置为 NULL,这表示对相应条 件不关心。如果这三个参数都设置为 NULL,则可以将 select()当做为一个类似于 sleep()休眠的函数来使用, 通过 select()函数的最后一个参数 timeout 来设置休眠时间。 select()函数的第一个参数 nfds 通常表示最大文件描述符编号值加 1,考虑 readfds、writefds 以及 exceptfds 这三个文件描述符集合,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是参数 nfds。 select()函数的最后一个参数 timeout 可用于设定 select()阻塞的时间上限,控制 select 的阻塞行为,可将 timeout 参数设置为 NULL,表示 select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;

poll函数

与select()函数相似,但是函数接口不同,在该函数中,我们需要构建一个struct poolfd类型的数组。

#include <poll.h>

int poll(struct pollfd *fds,nfds_t nfds,int timeout);

参数含义如下:

异步IO

在 I/O 多路复用中,进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。 而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程 就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。异步IO通常被称为信号驱动IO。

在调用 open()时无法通过指定 O_ASYNC 标志来使能异步 I/O,但可以使用 fcntl()函数 添加 O_ASYNC 标志使能异步 I/O,譬如:

设置异步I/O事件的接收进程:

为文件描述符设置异步IO事件的接收进程,也就是设置异步IO的所有者。同样也是通过fcntl()函数进行设置,操作命令cmd设置F_SETOWN,第三个参数传入接收进程的进程ID(PID),通常将调用进程的PID传入,譬如:

fcntl(fd,F_SETOWN,getpid());

1.注册SIGIO信号的处理函数

通过signal()和sigaction()函数为SIGIO信号注册一个信号处理函数,当进程收到内核发送过来的SIGIO信号时,会执行该处理函数,所以我们以后刚刚在处理函数当中执行相应的I/O操作。

优化异步IO:使用实时信号替代默认信号SIGIO,SIGIO 是一个非实时信号,可以通过fcntl()函数进行设置,调用函数时将操作命令cmd参数设置为F_SETSIG,第三个参数指定一个实时编号即可,譬如:

fcntl(fd,F_SETSIG,SIGRTMIN);

2. 使用sigaction()函数注册信号处理函数。

存储映射I/O

mmap()和munmap()函数

为了实现存储映射IO,我们需要告诉内核将一个给定的问价映射到进程地址空间中的一块内存区域中,这由系统调用mmap()来实现,函数原型如下:

#include<sys/mman.h>

void *mmap(void *addr,size_t length,int port,int fd,off_t offset);

参数及返回值含义如下:

addr:该参数用于指定映射到内存区域的起始地址,通常设置为NULL,这表示由系统选择该映射区的起始地址。如果参数部位NULL则表示由自己指定映射区的起始地址。此函数的返回值该映射区域的起始地址。

length:该参数指定映射长度,表示将文件中的多大部分映射到内存区域,以字节为单位。

offset:文件映射的偏移量,通常设置为0,表示从问价同步开始映射,

fd:文件描述符

port:指定了映射区域的保护要求,可取值如下:

⚫ PROT_EXEC:映射区可执行;

⚫ PROT_READ:映射区可读;

⚫ PROT_WRITE:映射区可写;

⚫ PROT_NONE:映射区不可访问。

对指定映射区的保护要求不能超过文件 open()时的访问权限,譬 如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。‘

flags:该参数可影响映射区域的多种属性,参数flags必须要指定以下两种标志之一:

⚫MAP_SHARED:此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映 射区中的数据更新到文件中,并且允许其它进程共享。

⚫ MAP_PRIVATE:此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-onwrite),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。 除此之外,还可将以下标志中的 0 个或多个组合到参数 flags 中,通过按位或运算符进行组合:

⚫ MAP_FIXED:在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定 映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的 值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的 值作为起始地址,如果使用指定值无法成功建立映射时,则放弃!通常,不建议使用此标志,因为 这不利于移植。

⚫ MAP_ANONYMOUS:建立匿名映射,此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域 无法和其它进程共享。

⚫ MAP_ANON:与 MAP_ANONYMOUS 标志同义,不建议使用。

⚫ MAP_DENYWRITE:该标志被忽略。

⚫ MAP_EXECUTABLE:该标志被忽略。

⚫ MAP_FILE:兼容性标志,已被忽略。

⚫ MAP_LOCKED:对映射区域进行上锁。

返回值:成功返回映射区域的起始值,发生错误时返回(void*)-1,通常使用MAP_FALED 来表示。

对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系 统页大小的整数倍,可通过 sysconf()函数获取页大小:

sysconf(_SC_PAEG_SIZE)或者sysconf(_SC_PAEGSIZE)

与映射相关的两个信号:

SIGEGV:如果映射区被指定成只读,那么在进程试图将数据写入映射区时,会产生该信号。

SIGBUS:如果映射区的某个部分在访问时不在,会产生该信号。

munmap()解除映射

当某个映射不再需要时,可以通过该函数解除映射关系,函数原型如下:

#include<sys/mman.h>

int munmap(void *addr,size_t length);

addr是映射的起始地址。当进程终止时也会自动解除映射(如果程序没有调用函数解除关系),但调用close()关闭文件时不会解除映射。

mprotect()函数

用于更改一个现有映射区的保护要求,函数原型如下:

#include<sys/mman.h>

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

成功调用返回0,失败返回-1,并设置errno.

msync()函数

将映射区域中的数据刷写,更新至磁盘文件中(同步操作),函数原型如下:

#include<sys/mman.h>

int msync(void addr,size_t length,int flags);

参数flags应指定为MS_ASYNC和MS_SYNC两个标志之一,除此以外还可以根据需求选择是否指定MS_INVALIDATE标志,

MS_ASYNC:以异步方式进行同步,调用函数后不会等待数据完全写入磁盘后才返回。

MS_SYNC:以同步方式进行同步操作,调用函数后,需等待数据全部写入磁盘后才返回。

MS_INVALIDATE:是一个可选标志,请求使同一文件的其它映射无效(以便可以用刚写入的值跟新它们)。

munmap()函数并不影响被映射的文件,也就是说,当调用 munmap()解除映射时并不会将映射区中的内 容写到磁盘文件中。如果 mmap()指定了 MAP_SHARED 标志,对于文件的更新,会在我们将数据写入到映 射区之后的某个时刻将映射区中的数据更新到磁盘文件中,由内核根据虚拟存储算法自动进行。 如果 mmap()指定了 MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃!

文件锁

用于避免多个进程同时操作同一文件。文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争 状态。

文件锁分类

建议性锁:

本质上是一种协议,程序访问文件前先对文件上锁,上锁成功后再访问文件。但是在没有对文件上锁的情况下直接进行访问文件,也是可以访问的。

强制性锁:

与建议性相反,在没有获取到文件锁时是无法对文件进行访问的。

flock()函数加锁

该函数可以对文件进行加锁或者解锁。函数原型如下:

#include<sys/file.h>

int flock(int fd,int operation);

参数和返回值含义:

fd:文件描述符

operation:指定了操作方法,可以设置为以下一个:

LOCK_SH:在fd引用的文件上放置一把共享锁,该共享锁可以被多个文件同时拥有。

LOCK_EX:放置一把排它锁(互斥锁),只能被一个进程拥有。

LOCK_NB:以非阻塞方式获取锁。没有获取到锁的进程不会被阻塞。

返回值:成功返回0,失败返回-1,并设置errno.

注意:同一个文件不会同时具有共享锁和互斥锁。

除此之外,当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过 复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解 锁都可以。

规则:1同一进程对文件多次加锁不会导致死锁

2.文件关闭的时候,会自动解锁

3.一个进程不可以对另一个进程持有的文件锁进行解锁。

4.由 fork()创建的子进程不会继承父进程所创建的锁。

fcntl()函数加锁

函数原型如下:

#include<unistd.h>

#include<fcntl.h>

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

与锁相关的 cmd 为 F_SETLK、F_SETLKW、F_GETLK,第三个参数 flockptr 是一个 struct flock 结构体 指针。使用 fcntl()实现文件锁功能与 flock()有两个比较大的区别: ⚫ flock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁 /解锁,可以精确到某一个字节数据。 ⚫ flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。

结构体 flock定义如下:

struct flock { ...

short l_type; /* Type of lock: F_RDLCK,F_WRLCK, F_UNLCK */

short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */

off_t l_start; /* Starting offset for lock */ off_t l_len; /*

Number of bytes to lock */ pid_t l_pid; /*

PID of process blocking our lock(set by F_GETLK and F_OFD_GETLK) */ .

..

};

对 struct flock 结构体说明如下:

⚫ l_type:所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一,F_RDLCK 表示共享性质的读锁,F_WRLCK 表示独占性质的写锁,F_UNLCK 表示解锁一个区域。

⚫ l_whence 和 l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量,与 2.7 小节所学 的 lseek()函数中的 offset 和 whence 参数相同 

⚫ l_len:需要加锁或解锁区域的字节长度。

⚫ l_pid:一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。

以上便是对 struct flock 结构体各成员变量的简单介绍,对于加锁和解锁区域的说明,还需要注意以下 几项规则:

⚫ 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。 ⚫ 若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文 件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文 件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。

⚫ 如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置,并且指定 参数 l_len 等于 0。

上面我们提到了两种类型的锁,分别为共享性读锁(F_RDLCK)和独占性写锁(F_WRLCK)。任意多个进程在一个给定的字节上可以有一把共享的读 锁,但是在一个给定的字节上只能有一个进程有一把独占写锁,进一步而言,如果在一个给定的字节上已经 有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加 任何锁(包括读锁和写锁)。

如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新 加的锁将替换旧的锁。还需要注意另外一个问题,当对文件的某一区域加读锁时,调用进程必须对该文件有读权限,譬如 open() 时 flags 参数指定了 O_RDONLY 或 O_RDWR;当对文件的某一区域加写锁时,调用进程必须对该文件有写 权限,譬如 open()时 flags 参数指定了 O_WRONLY 或 O_RDWR。

F_SETLK、F_SETLKW 和 F_GETLK 我们来看看与文件锁相关的三个 cmd 它们的作用:

⚫ F_GETLK:这种用法一般用于测试,测试调用进程对文件加一把由参数 flockptr 指向的 struct flock 对象所描述的锁是否会加锁成功。如果加锁不成功,意味着该文件的这部分区域已经存在一把锁, 并且由另一进程所持有,并且调用进程加的锁与现有锁之间存在排斥关系,现有锁会阻止调用进程 想要加的锁,并且现有锁的信息将会重写参数 flockptr 指向的对象信息。如果不存在这种情况,也 就是说 flockptr 指向的 struct flock 对象所描述的锁会加锁成功,则除了将 struct flock 对象的 l_type 修改为 F_UNLCK 之外,结构体中的其它信息保持不变。

⚫ F_SETLK:对文件添加由 flockptr 指向的 struct flock 对象所描述的锁。譬如试图对文件的某一区 域加读锁(l_type 等于 F_RDLCK)或写锁(l_type 等于 F_WRLCK),如果加锁失败,那么 fcntl() 将立即出错返回,此时将 errno 设置为 EACCES 或 EAGAIN。也可用于清除由 flockptr 指向的 struct flock 对象所描述的锁(l_type 等于 F_UNLCK)。

⚫ F_SETLKW:此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读 锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进 程将会进入阻塞状态。只有当请求的锁可用时,进程才会被唤醒。

F_GETLK 命令一般很少用,事先用 F_GETLK 命令测试是否能够对文件加锁,然后再用 F_SETLK 或 F_SETLKW 命令对文件加锁,但这两者并不是原子操作,所以即使测试结果表明可以加锁成功,但是在使 用 F_SETLK 或 F_SETLKW 命令对文件加锁之前也有可能被其它进程锁住。

关于使用 fcntl()创建锁的几条规则与 flock()相似,如下所示:

⚫ 文件关闭的时候,会自动解锁。

⚫ 一个进程不可以对另一个进程持有的文件锁进行解锁。

⚫ 由 fork()创建的子进程不会继承父进程所创建的锁。

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值