嵌入式Linux中高级I/O的常用知识点

top命令可以查看应用进程的cpu使用率,类似与window的任务管理器

高级I/O

非阻塞IO

阻塞IO与非阻塞IO读文件

对文件的打开方式来说明对后续的文件IO操作是阻塞形式的还是非阻塞形式的,默认是阻塞形式,若要指定为非阻塞形式则在open()函数中用O_NONBLOCK指定。

对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会
阻塞的,它总是以非阻塞的方式进行 I/O 操作 。

对于输入设备它的设备文件都在/dev/input/目录下,在该目录下有许多文件,为了判断哪个文件是我们要找的文件就需要使用od命令

sudo od -x /dev/input/xxx	# xxx表示目录下的文件

当执行命令之后,操作相应的设备、都会在终端打印出相应的数据 ,如果没有打印信息,那么这个设备文件就不是对应的设备文件,那么就换一个设备文件再次测试,这样就会帮助你找到需要的设备文件。

ps:因为标准输入文件描述符(键盘)是从其父进程继承而来,并不是在我们的程序中调用 open()打开得到的, 所以要将标准输入设置为非阻塞IO,需要用fcntl()函数

int flag;
flag = fcntl(0, F_GETFL); 	//先获取原来的 flag
flag |= O_NONBLOCK; 		//将 O_NONBLOCK 标志添加到 flag
fcntl(0, F_SETFL, flag); 	//重新设置 flag

使用非阻塞IO实现并发读取

用于同时读取多个输入设备的方法,把输入设备都以非阻塞方式打开,然后使用轮询的方式读取数据

副作用cpu占用率高

IO多路复用

IO多路复用用于解决[使用非阻塞IO实现并发读取](## 使用非阻塞IO实现并发读取)中cpu占用率高的问题

I/O 多路复用(IO multiplexing) 它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件) 可以执行 I/O 操作时, 能够通知应用程序进行相应的读写操作。由此可知, I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取

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

ps:由于select()函数和poll()函数相似,且觉得poll()用起来更简单所以主要介绍poll()

  • poll()函数

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

    fds: 指向一个 struct pollfd 类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件,稍后介绍 struct pollfd 结构体类型。

    nfds: 参数 nfds 指定了 fds 数组中的元素个数,数据类型 nfds_t 实际为无符号整形。

    timeout:等于-1,一直阻塞直到 fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。

    struct pollfd {
    	int fd; 		
        //初始化 events 来指定需要为文件描述符 fd 做检查的事件
    	short events; 
        //revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件
    	short revents; 
    }
    
标志名输入至events从revents得到结果说明
POLLIN**有数据可以读取
POLLPRI**可读取高优先级数据
POLLOUT**可写入数据

可以用‘|’把多个标志组合起来

**poll()函数返回值 **

  • 返回-1 表示有错误发生,并且会设置 errno。

  • 返回 0 表示该调用在任意一个文件描述符成为就绪态之前就超时了。

  • 返回一个正整数表示有一个或多个文件描述符处于就绪态了, 返回值表示 fds 数组中返回的 revents
    变量不为 0 的 struct pollfd 对象的数量

    //对fds数字的初始化
    fds[0].fd = 0;
    fds[0].events = POLLIN; //只关心数据可读
    fds[0].revents = 0;
    fds[1].fd = fd;
    fds[1].events = POLLIN; //只关心数据可读
    fds[1].revents = 0;
    
    //检查文件是否为就绪态
    if(fds[0].revents & POLLIN)
    

    总结

    当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在 ,那么下一次调用 poll()时,文件描述符已经处于就绪态了,将直接返回。

异步IO

多路复用IOselect()poll()的返回值表示有多少个文件描述符处于就绪态了,但并没有告诉我们具体是哪几个文件描述符处于就绪态,所以我们需要自己去遍历所有文件描述符来找到处于就绪态的文件描述符,若文件描述符有几百甚至上千就会导致程序效率低下,所以引入异步IO概念

异步IO:当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。 之后进程
就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。

步骤

  • 通过指定 O_NONBLOCK 标志使能非阻塞 I/O。

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

    int flag;
    flag = fcntl(0, F_GETFL); 	//先获取原来的 flag
    flag |= O_ASYNC; 			//将 O_ASYNC 标志添加到 flag
    fcntl(fd, F_SETFL, flag); 	//重新设置 flag
    
  • 设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程,
    通常将调用进程设置为异步 I/O 事件的接收进程。

fcntl(fd, F_SETOWN, getpid());	//设置异步 I/O 事件的接收进程
  • 为内核发送的通知信号注册一个信号处理函数。默认情况下, 异步 I/O 的通知信号是 SIGIO,所以
    内核会给进程发送信号 SIGIO。但是SIGIO 信号是标准信号(非实时信号、不可靠信号),所以它不支持信号排队机制,譬如当前正在执行 SIGIO 信号的处理函数,此时内核又发送多次 SIGIO 信号给进程,这些信号将会被阻塞,只有当信号处理函数执行完毕之后才会传递给进程,并且只能传递一次,而其它后续的信号都会丢失。也无法得知文件描述符发生了什么事件(是可读取还是可写入亦或者发生异常)所以需要使用实时信号代替默认信号SIGIO

    • 首先需要指定一个实时信号或者说是注册一个实时信号,因为实时信号都是数字编号而没有名字

      //正常的fcntl()是没有F_SETSIG标志的,
      //所以要使用这个标志需要在源文件的开头
      //定义一个宏_GNU_SOURCE
      
      #define _GNU_SOURCE
      
      fcntl(fd, F_SETSIG, SIGRTMIN);	//注册了一个实时信号SIGRTMIN
      
    • 使用 sigaction()函数注册信号处理函数

      int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
      
      struct sigaction {
      	void (*sa_handler)(int);//不用
      	void (*sa_sigaction)(int, siginfo_t *, void *);
      	sigset_t sa_mask;
      	int sa_flags;				
          //要指定sa_flags=SA_SIGINFO,使用sa_sigaction()作为信号处理函数
      	void (*sa_restorer)(void);	//过时,别用
      };
      

      signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。

      act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式

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

      ==void (*sa_sigaction)(int, siginfo_t *, void *);==中siginfo_t结构体介绍

      • si_signo:引发处理函数被调用的信号。这个值与信号处理函数的第一个参数一致。

      • si_fd:表示发生异步 I/O 事件的文件描述符;

      • si_code:表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或者是异常事件等。

      • si_band:是一个位掩码, 其中包含的值与系统调用 poll()中返回的 revents 字段中的值相同。

        si_codesi_band 掩码值描述/说明
        POLL_INPOLLIN | POLLRDNORM可读取数据
        POLL_OUTPOLLOUT | POLLWRNORM | POLLWRBAND可写入数据
        POLL_MSGPOLLIN | POLLRDNORM | POLLMSG不使用
        POLL_ERRPOLLERRI/O 错误
        POLL_PRIPOLLPRI | POLLRDNORM可读取高优先级数据
        POLL_HUPPOLLHUP | POLLERR出现宕机

存储映射IO

存储映射IO的作用是提高IO的读写效率,在需要大量处理数据时有用,在刚开始学习存储映射IO时觉得它没有太多作用,所以看的很快直接到看到最后时,知道它的应用场景(存储映射 I/O 会在视频图像处理方面用的比较多)才发现自己一开始的错误观念是重新看了一遍,所以为了提高读者的重视性把结论写在了开头。

存储映射 I/O(memory-mapped I/O) 是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程
地址空间中的一块内存区域中 ,然后对内存操作就是对文件操作而不需要用read()write()了从而提高对文件的读写效率。

步骤

  1. open()打开需要进行存储映射的文件

  2. 使用mmap()把打开的文件映射到进程地址空间中

  3. 使用munmap()来解除映射,当进程终止时也会解除映射(隐射),但使用close()不会解除映射

  • mmap()

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

    addr: 参数 addr 用于指定映射到内存区域的起始地址。通常将其设置为 NULL,这表示由系统选择该
    映射区的起始地址

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

    offset: 文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射

    fd: 文件描述符,指定要映射到内存区域中的文件

    prot: 参数 prot 指定了映射区的保护要求

    • PROT_EXEC: 映射区可执行;
    • PROT_READ: 映射区可读;
    • PROT_WRITE: 映射区可写;
    • PROT_NONE: 映射区不可访问。

对指定映射区的保护要求不能超过文件 open()时的访问权限

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

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

MAP_PRIVATE: 此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy- onwrite),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写 。如果 mmap()指定了 MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃!

  • **munmap()**解除映射

    int munmap(void *addr, size_t length);
    

    psmunmap()函数并不影响被映射的文件,也就是说,当调用 munmap()解除映射时并不会将映射区中的内容写到磁盘文件中。当指定MAP_SHARED 标志时由mmap()来把映射区的内容更新至磁盘文件;如果 mmap()指定了MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃!所以在解除映射区前最好用msync()同步一下

  • **mprotect()**函数

    //可以更改现有映射区的保护要求
    //mprotect()函数调用成功返回 0; 失败将返回-1,并且会设置 errno 来只是错误原因
    int mprotect(void *addr, size_t len, int prot);
    
  • **msync()**函数

    //sync族函数,用于同步
    //msync()函数在调用成功情况下返回 0;失败将返回-1、并设置 errno
    int msync(void *addr, size_t length, int flags)
    

    flagsMS_ASYNC MS_SYNC 两个标志之一

    • MS_ASYNC: 以异步方式进行同步操作。调用 msync()函数之后,并不会等待数据完全写入磁盘之
      后才返回。
    • MS_SYNC: 以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘之后才
      返回

文件锁

前面四种IO处理方式主要是解决单进程对多个文件的处理操作,而文件锁主要是用在多进程读写一个文件中,与多线程处理共享资源类似

文件锁分为建议性锁强制性锁,建议性锁的意思是如果一个进程把一个文件锁上了,但另一个进程如果不遵守锁的规则的话还是可以对文件读写,“防君子不防小人”;而强制性锁如果一个进程被锁上了,另一个进程必然不能对它进行读写。

一共有三个函数可以加锁:flock()fcntl()lockf(),这里只介绍fcntl(),因为它功能最强大,其它两个能做的事他都能做,减少记忆负担

**fcntl()**函数加锁

  1. fcntl()可以对文件的某个区域(某部分内容)进行加锁/解锁,可以精确到某一个字节数据
  2. fcntl()可支持建议性锁和强制性锁两种类型
//cmd 为 F_SETLK、 F_SETLKW、 F_GETLK
//第三个参数 flockptr 是一个 struct flock 结构体指针
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
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) 	*/
	...
};
  • l_type: 所希望的锁类型,可以设置为 F_RDLCKF_WRLCKF_UNLCK 三种类型之一
  • l_whencel_start: 这两个变量用于指定要加锁或解锁区域的起始字节偏移量
  • l_len: 需要加锁或解锁区域的字节长度。
/* 对文件加锁 */
lock.l_type = F_WRLCK; //独占性写锁
lock.l_whence = SEEK_SET; //文件头部
lock.l_start = 0; //偏移量为 0
lock.l_len = 0;
if (-1 == fcntl(fd, F_SETLK, &lock)) {
	perror("加锁失败");
	exit(-1);
}

注意

  • 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。

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

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

加锁规则

任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定的字节上只能有一个进程有一把占写锁,进一步而言,如果在一个给定的字节上已经有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何锁(包括读锁和写锁)

三个cmd参数:F_SETLKF_SETLKWF_GETLK

  • 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 指向的 structflock 对象所描述的锁(l_type 等于 F_UNLCK)
  • F_SETLKW: 此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读
    锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进
    程将会进入阻塞状态。 只有当请求的锁可用时,进程才会被唤醒。

ps

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

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

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

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

  • 如果两个区域出现了重叠,譬如 100~200 字节区间和 150~250 字节区间, 150~200 就是它们的重叠部分,一个进程对同一文件的相同区域不可能同时加两把锁,新加的锁会把旧的锁替换掉,譬如100~200字节区间加写锁、再对 150~250 字节区间加读锁,那么 150~200 字节区间最终是读锁控制的

强制性锁和建议性锁

前面我们提到了 fcntl()支持强制性锁和建议性锁, 但是一般不建议使用强制性锁,所以大部分情况下使
用的都是建议性锁

对于一个特定的文件,开启它的强制性锁机制其实非常简单,主要跟文件的权限位有关系如果要开启强制性锁机制,需要设置文件的 Set-GroupID(S_ISGID)位为 1,并且禁止文件的组用户执行权限(S_IXGRP),也就是将其设置为 0

/* 开启强制性锁机制 */
if (0 > fstat(fd, &sbuf)) {//获取文件属性
	perror("fstat error");
	exit(-1);
}
if (0 > fchmod(fd, (sbuf.st_mode & ~S_IXGRP)
| S_ISGID)) {
	perror("fchmod error");
	exit(-1);
}

性锁和建议性锁, 但是一般不建议使用强制性锁,所以大部分情况下使
用的都是建议性锁

对于一个特定的文件,开启它的强制性锁机制其实非常简单,主要跟文件的权限位有关系如果要开启强制性锁机制,需要设置文件的 Set-GroupID(S_ISGID)位为 1,并且禁止文件的组用户执行权限(S_IXGRP),也就是将其设置为 0

/* 开启强制性锁机制 */
if (0 > fstat(fd, &sbuf)) {//获取文件属性
	perror("fstat error");
	exit(-1);
}
if (0 > fchmod(fd, (sbuf.st_mode & ~S_IXGRP)
| S_ISGID)) {
	perror("fchmod error");
	exit(-1);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值