Linux阻塞和非阻塞IO

一. 阻塞和非阻塞简介
  1. IO指的是输入/输出的意思,即应用程序对驱动程序的输入/输出操作

  2. 应用程序不能获取到设备资源

    • 阻塞式IO会将应用程序对应的线程挂起,直到获取到设备资源
    • 非阻塞式IO则不会挂起对应线程,而是一直轮询等待,直到获取到设备资源或直接放弃
  3. 阻塞IO示意图在这里插入图片描述

  4. 非阻塞IO示意图在这里插入图片描述

  5. 应用程序阻塞式和非阻塞式访问驱动程序

    /* 对于驱动设备,默认是以阻塞的方式打开的 */
    fd = open("/dev/xxx_dev", O_RDWR);
    /* 若想以非阻塞式方式打开,需要加入相关参数 */
    fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
    
二. 等待队列
  1. 阻塞访问的好处是,当设备文件不可操作时,应用程序可以进入阻塞态让出CPU资源。当设备文件可操作时必须唤醒进程,一般在中断服务函数中唤醒。Linux内核提供等待队列wait queue)来实现阻塞进程的唤醒工作。

  2. 等待队列头,即一个等待队列的头部,使用在include/linux/wait.h文件下的wait_queue_head_t结构体定义

    struct __wait_queue_head {
    	spinlock_t			lock;
    	struct list_head	task_list;
    };
    typedef struct __wait_queue_head wait_queue_head_t;
    
    /* 创建并初始化队列头,参数q为要初始化的队列头 */
    void init_waitqueue_head(wait_queue_head_t *q);
    
  3. 等待队列项,每个访问设备的进程都是一个等待队列项;当进程阻塞时,则将其添加到等待队列中;用结构体wait_queue_t表示等待队列项

    struct __wait_queue {
    	unsigned int		flags;
    	void				*private;
    	wait_queue_func_t	func;
    	struct list_head	task_list;
    };
    typedef struct __wait_queue wait_queue_t;
    
    /*  创建并初始化等待队列项
    	name表示等待队列项的名字
    	tsk表示该等待队列项属于哪个进程,一般为current,表示当前进程*/
    DECLARE_WAITQUEUE(name, tsk);
    
  4. 添加/移除等待队列项:当设备不可访问时,要将进程对应的等待队列项插入等待队列中,进程才可以进入休眠状态;当设备可以访问时,再将其移除出等待队列

    /* 参数q为要加入的等待队列(头),参数wait为要加入的等待队列项 */
    /* 将等待队列项添加至等待队列 */
    void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
    /* 将等待队列项移除出等待队列 */
    void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
    
  5. 主动唤醒等待队列

    TASK_INTERRUPTIBLE状态表示可被信号(硬件中断/软件信号)唤醒;TASK_INTERRUPTIBLE状态则不能被信号打断。

    /* 参数q为唤醒的等待队列头 */
    // 唤醒等待队列中所有的所有处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程
    void wake_up(wait_queue_head_t *q); 
    // 唤醒等待队列中所有的所有处于TASK_INTERRUPTIBLE状态的进程
    void wake_up_interruptible(wait_queue_head_t *q);
    
  6. 等待事件到来,唤醒等待队列中的进程

    /* 	将进程设置为TASK_UNINTERRUPTIBLE状态 
    	参数wq是以等待队列(头)
    	参数condition,为真时进程被唤醒,否则一直阻塞*/
    #define wait_event(wq, condition)
    /*  timeout为超时时间,单位为jiffies(系统时钟中断次数)
    	当condition为真时,返回1
    	当condition为假且timeout到了,返回0*/
    #define wait_event_timeout(wq, condition, timeout)
    
    /* 	将进程设置为TASK_INTERRUPTIBLE状态,即可以被信号打断*/
    #define wait_event_interruptible(wq, condition)
    #define wait_event_interruptible_timeout(wq, condition, timeout)
    
  7. 设置进程状态 __set_current_state()

三. 轮询
  1. 非阻塞方式访问设备,设备驱动程序需要提供非阻塞的处理方式,即轮询。

  2. select函数:能够监视的文件描述符最大数量为1024,并且加大数量会使效率降低

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

    nfds:所要监视的三类文件描述符集中,最大文件描述符**+1**

    fd_set:fd_set 类型变量(文件描述符集合)的每一个位都代表了一个文件描述符

    readfdswritefdsexceptfds:这三个fd_set类型指针指向描述符集合

    • readfds 用于监视指定描述符集变化,当集合中有一个文件可以读取,则selsect返回一个大于0的数。
    • writefds用于监视指定描述符集变化
    • exceptfds用于监视指定描述符集异常

    fd_set类型定义

    typedef __kernel_fd_set		fd_set;
    #undef __FD_SETSIZE
    #define __FD_SETSIZE	1024
    typedef struct {
    	unsigned long fds_bits[ __FD_SETSIZE / (8 * sizeof(long)) ];
    } __kernel_fd_set;
    

    操作fd_set变量

    void FD_ZERO(fd_set *set);			//清零fd_set变量,即清除所有文件描述符
    void FD_SET(int fd, fd_set *set);	//向fd_set添加set文件描述符
    void FD_CLR(int fd, fd_set *set);	//将set文件描述符从fd_set中删除
    int  FD_ISSET(int fd, fd_set *set);	//判断set文件描述符是否属于fd_set,1为匹配
    

    timeout:超时时间,当为NULL时,表示无限期等待

    struct timeval { 
        long tv_sec; 	/* 秒   */ 
        long tv_usec;	/* 微妙 */
    };
    

    返回值:0表示超时;-1表示发生错误;其他值表示可以进行操作文件个数

  • 举例

    int main()
    {
        int 	flag,fd;		//要监听的文件描述符
        fd_set	readfds;		//读操作文件描述符集
        struct 	timeval	timeout;//超时时间
        
        fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);	//以非阻塞形式打开文件
        
        FD_ZERO(&readfds);		//将fd加入读文件操作符集
        FD_SET(fd,&readfds);
        
        timeout.tv_sec  = 0;	//设置timeout时间
        timeout.tv_usec = 50000;//50ms
        
        flag = select(fd+1,&readfds,NULL,NULL,&timeout);	//轮询文件是否可操作
        if(flag == 0)		printf("timeout\r\n");
        else if(flag == -1)	printf("err\r\n");
        else
        {
            if(FD_ISSET(fd,&readfds)){		/*属于fd文件描述符*/
             	//读fd文件操作   
            }
        }
        
        return 0;
    }
    
  1. poll函数:与select函数相同,只是监视的文件描述符没有限制

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

    fds:要监视的文件描述符事件的集合,类型为pollfd

    struct pollfd {
        int fd;			/* 文件描述符 */
    	short events; 	/* 请求的事件 */
        short revents;  /* 返回的事件 */
    };
    
    /* events为监听的事件,可以为如下类型 */
    POLLIN 		有数据可以读取。
    POLLPRI 	有紧急的数据需要读取。
    POLLOUT		可以写数据。 
    POLLERR		指定的文件描述符发生错误。 
    POLLHUP 	指定的文件描述符挂起。
    POLLNVAL 	无效的请求。
    
    /* revents为返回参数,即返回的事件 */
    

    nfds:要监视的文件描述符数量

    timeout:超时时间

    返回值:0超时;-1错误;返回可操作文件的数量

  • 举例

    int main()
    {
        int 	flag,fd;		//要监听的文件描述符
        pollfd	readfds;		//读操作文件描述符
        
        fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);	//以非阻塞形式打开文件
        
        readfds.fd 		= fd;		//文件描述符是否可读取
        readfds.events	= POLLIN;
        
        flag = poll(&readfds,1,1000);	//轮询文件是否可操作
        if(flag == 0)		printf("超时\r\n");
        else if(flag == -1)	printf("错误\r\n");
        else				//对fd文件进行读操作
            
        return 0;
    }
    
  1. epoll函数:上述两个函数,都会随着所监听的fd数量的增多,而出现效率低下;epoll就是为了解决这个问题的,为处理大并发准备的,一般在网络编程中会使用。
四. Linux驱动下的poll操作函数
  1. 应用程序调用select和poll函数对驱动程序进行非阻塞访问时,就会调用file_operations操作集下的poll函数,所以驱动程序需要编写poll函数

    /* 	filp参数为要打开的设备文件
    	wait为poll_table_struct结构体指针,由应用程序传递进来 */
    unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
    
    /*返回值为设备或资源的状态*/
    POLLIN 		有数据可以读取。
    POLLPRI 	有紧急的数据需要读取。
    POLLOUT		可以写数据。 
    POLLERR		指定的文件描述符发生错误。 
    POLLHUP 	指定的文件描述符挂起。
    POLLNVAL 	无效的请求。
    0			超时
    
  2. 在poll函数中,调用poll_wait函数;该函数不会引起阻塞,只是将应用程序添加到poll_table

    /* wait_address为要添加到poll_table中的等待队列头
    	p为poll_table,即为poll函数中的wait参数*/
    void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
    
  3. 当APP使用pollselect系统调用时,会先调用poll_initwait(&table),初始化poll_table;然后调用file_operations->poll函数,然后使用poll_wait函数,将等待队列头加入poll_table(将current添加到等待队列头);随后调用poll_schedule_timeout (),到时后,返回运行file_operations->poll函数,最后完成判断事件操作后,返回结果至APP,同时调用poll_freewait(&table)

    /* file_operations->poll函数示例 */
    static unsigned int Dev_poll(struct file *filp, struct poll_table_struct *wait)
    {
    	struct LedDev_t *dev = filp->private_data;
    
    	/* 将等待队列头添加到poll_table中 */
    	poll_wait(filp, &queue_head, wait);
        
    	if()						//判断是否可以读取
    		return POLLIN;
        else if()					//判断是否可以写
            return POLLOUT;
    	else return 0;				//timeout
    }
    
五.小结
  • 阻塞IO
    • 创建等待队列头
    • 当某个条件/事件未达到,为当前进程创建等待队列项;设置进程状态,将进程等待队列项插入等待队列头(进入休眠)
    • 进行一次任务调度
    • 在某个事件到来时(如中断),唤醒等待队列
    • 将进程等待队列项移出等待队列头,设置进程状态,进程继续运行
  • 非阻塞IO
    • 以非阻塞的形式打开文件
    • App使用pollselect函数,轮询文件是否可以操作,设置轮询时间(根据这个轮询时间,可以大大减小cpu占用)
    • 对应驱动层,则实现poll函数

以上是我在学习过程中的总结,不当之处请在评论区指出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值