十、(正点原子)Linux阻塞和非阻塞IO

        阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞。这里的“IO”并不是我们学习 STM32 或者其他单片机的时候所说的“GPIO”(也就是引脚)。这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。

一、阻塞IO

        1、简介

        应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。

        应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。

        注:当我们应用程序打开驱动程序时候,默认以阻塞方式打开的。

        2、等待队列

        (1)、等待队列头

        阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:

struct __wait_queue_head {
	spinlock_t		    lock;            /* 自旋锁 */
	struct list_head	task_list;       /*  指向等待队列的头 */
};
typedef struct __wait_queue_head wait_queue_head_t;

        定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头,函
数原型如下:

#define init_waitqueue_head(q)				\
	do {						\
		static struct lock_class_key __key;	\
							\
		__init_waitqueue_head((q), #q, &__key);	\
	} while (0)

void __init_waitqueue_head(wait_queue_head_t *q, const char *name, 
                            struct lock_class_key *key)


/* 最终函数原型为 */
void init_waitqueue_head(wait_queue_head_t *q)

        q:要初始化的等待队列头 。

        也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化,定义在linux/wait.h里面,函数原型如下:

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

        name:要初始化的等待队列头。

        (2)、等待队列项

        等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项,定义在linux/wait.h里面,结构体内容如下:

typedef struct __wait_queue    wait_queue_t;

struct __wait_queue {
	unsigned int		flags;      /* prepare_to_wait()里有对flags的操作,查看以得出其含义 */
	void			*private;       /* 通常指向当前任务控制块 */
	wait_queue_func_t	func;       /* 唤醒阻塞任务的函数 ,决定了唤醒的方式 */
	struct list_head	task_list;  /* 阻塞任务链表 */
};

        使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,定义在linux/wait.h里面,宏定义的内容如下:

#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

        name:等待队列项的名字。

        tsk:表示等待队列项属于哪个任务(进程),一般设置为current, 在Linux内核中current相当于一个全局变量,表示当前进程。

因此宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

        (3)、将等待队列项添加/移除等待队列头

        当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可。

        1)、将队列项添加

        使用API函数定义在linux/wait.h里,如下:

extern void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

        q: 要添加到哪个等待队列头。

        wait:要添加的等待队列项。

        2)、将队列项移除

        使用API函数定义在linux/wait.h里,如下:

extern void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

         q: 要移除的等待队列项属于哪个等待队列头。

        wait:要移除的等待队列项。

注:在将等待队列项添加到等待队列头后,还需要两步操作:

__set_current_state(TASK_INTERRUPTIBLE);    /* 设置当前等待队列项为信号可打断 */
schedule();                                 /* 切换,等待被唤醒 */

函数如果读数据时数据正忙,就会卡在这里进入休眠态,当进程被唤醒后,从schedule函数开始执行代码,还需要判断进程时如何被唤醒的:

/* 唤醒从这里运行 */
if (signal_pending(current)) {              /* 判断当前任务唤醒条件,为真表示信号唤醒 */
    ret = -ERESTARTSYS;
    return ret;
}

        (4)、等待唤醒

        当设备不可用时,进程进入休眠态,当设备可以使用时候,就要唤醒就休眠态的进程。使用如下两个函数唤醒休眠态进程,定义在linux/wait.h里,定义如下:

1、
#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key);

/* 最终形式 */
void wake_up(wait_queue_head_t *q)


2、
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key);

/* 最终形式 */
void wake_up_interruptible(wait_queue_head_t *q)

        q:要唤醒的等待队列头。 

        这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程.

        进程的状态定义在linux/sched.h里面如下:

/*
 * Task state bitmask. NOTE! These bits are also
 * encoded in fs/proc/array.c: get_task_state().
 *
 * We have two separate sets of flags: task->state
 * is about runnability, while task->exit_state are
 * about the task exiting. Confusing, but this way
 * modifying one set can't modify the other one by
 * mistake.
 */
#define TASK_RUNNING		0
#define TASK_INTERRUPTIBLE	1
#define TASK_UNINTERRUPTIBLE	2
#define __TASK_STOPPED		4
#define __TASK_TRACED		8
/* in tsk->exit_state */
#define EXIT_DEAD		16
#define EXIT_ZOMBIE		32
#define EXIT_TRACE		(EXIT_ZOMBIE | EXIT_DEAD)
/* in tsk->state again */
#define TASK_DEAD		64
#define TASK_WAKEKILL		128
#define TASK_WAKING		256
#define TASK_PARKED		512
#define TASK_STATE_MAX		1024

#define TASK_STATE_TO_CHAR_STR "RSDTtXZxKWP"

        (5)、等待事件

        除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。和等待事件有关的 API 函数定义在linux/wait.h里面

        1)、wait_event函数

        等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞 。 此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态。

#define wait_event(wq, condition)					\
do {									\
	might_sleep();							\
	if (condition)							\
		break;							\
	__wait_event(wq, condition);					\
} while (0)

        wq: 等待队列头。

        condition:唤醒的条件。

        2)、wait_event_interruptible函数

        与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。

#define __wait_event_interruptible(wq, condition)			\
	___wait_event(wq, condition, TASK_INTERRUPTIBLE, 0, 0,		\
		      schedule())

        wq: 等待队列头。

        condition:唤醒的条件。

        3)、wait_event_timeout函数

        功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition为假。为 1 的话表示 condition 为真,也就是条件满足了。

#define __wait_event_timeout(wq, condition, timeout)			\
	___wait_event(wq, ___wait_cond_timeout(condition),		\
		      TASK_UNINTERRUPTIBLE, 0, timeout,			\
		      __ret = schedule_timeout(__ret))

        wq: 等待队列头。

        condition:唤醒的条件。

        timeout:设置超时时间。

        4)、wait_event_interruptible_timeout函数

        与 wait_event_timeout 函数类似,此函数也将进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。

#define wait_event_interruptible_timeout(wq, condition, timeout)	\
({									\
	long __ret = timeout;						\
	might_sleep();							\
	if (!___wait_cond_timeout(condition))				\
		__ret = __wait_event_interruptible_timeout(wq,		\
						condition, timeout);	\
	__ret;								\
})

        wq: 等待队列头

        condition:唤醒的条件。

        timeout:设置超时时间。

        (6)、使用案例

/* 第一步定义和初始化等待列表头
 * 第二步在读写操作判断怎么读写(阻塞非阻塞)
 * 第三步在中断处理函数唤醒
 *
 */

wait_queue_head_t     r_wait

/* 读操作 */
ssize_t xxx_read (struct file *filep, char __user *buf, size_t cnt, loff_t *plot)
{
    /* 判断阻塞读还是非阻塞读 */
    if(条件 & O_NONBLOCK) {  /* 非阻塞读 */
    
    }else{                    /* 阻塞读 */
        wait_event_interruptible(r_wait, 条件);   /* 可被信号打断 */
    }
}


/* 驱动入口函数 */
int __init xxx_init(void)
{

    init_waitqueue_head(&r_wait);
}

/* 中断处理函数 */
irqreturn_t xxx(int irq, void *dev_id)
{
    if(唤醒条件){
        wake_up(&r_wait);
    }
}

二、非阻塞IO

        1、简介

        当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。

        应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。

        如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:

int fd;
int data = 0;

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data));         /* 读取数据 */

        2、轮询

        如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。 poll epoll select 可以用于处理轮询,应用程序通过 selectepoll poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 selectepollpoll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。

        (1)、select函数

        select 函数在linux下原型如下:

#include <sys/select.h>

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

         nfds:所要监视的这三类文件描述集合中, 最大文件描述符加 1。

        readfds:用于监视指定描述符集合的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。

        writefds、exceptfds: readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。 exceptfds 用于监视这些文件的异常。

        timerout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 struct timeval 表示,结构体定义如下所示:

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

        返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数。

        使用select函数对某个驱动文件进行非阻塞读应用程序示例:

void main(void)
{
    int ret, fd; /* 要监视的文件描述符 */
    fd_set readfds; /* 读操作文件描述符集 */
    struct timeval timeout; /* 超时结构体 */
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */

    FD_ZERO(&readfds); /* 清除 readfds */
    FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */

    /* 构造超时时间 */
    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */

    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch (ret) {
        case 0: /* 超时 */
            printf("timeout!\r\n");
            break;
        case -1: /* 错误 */
            printf("error!\r\n");
            break;
        default: /* 可以读取数据 */
            if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
                /* 使用 read 函数读取数据 */
            }
            break;
    }
}

        (2)、poll函数

        在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数,poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制, Linux 应用程序中 poll 函数原型如下所示:

#include <poll.h>

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

         fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的, pollfd 结构体如下所示:

struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 请求的事件 */
    short revents; /* 返回的事件 */
};

        fd :是要监视的文件描述符,如果fd无效的话那么events监视事件也就无效,并且revents
返回 0。revents是返回参数,也就是返回的事件,由 Linux 内核设置具体的返回事件。events是要监视的事件,可监视的事件类型如下所示:

POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN

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

        timeout:超时时间,单位为 ms。

        返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并且设置errno为错误类型。

        使用 poll 函数对某个设备驱动文件进行非阻塞读访问的示例代码:

void main(void)
{
    int ret;
    int fd; /* 要监视的文件描述符 */
    struct pollfd fds;

    fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */

    /* 构造结构体 */
    fds.fd = fd;
    fds.events = POLLIN; /* 监视数据是否可以读取 */
    ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
    if (ret) { /* 数据有效 */
         ......
         /* 读取数据 */
         ......
     } else if (ret == 0) { /* 超时 */
         ......
     } else if (ret < 0) { /* 错误 */
     ......
     }
}

        (3)、epoll函数

        传统的selcetpoll函数都会随着所监听的fd数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll应运而生,epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。

         应用程序需要先使用epoll_create函数创建一个 epoll 句柄, epoll_create函数原型如下:

int epoll_create(int size)

         size: 从Linux2.6.8开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。

        返回值: epoll 句柄,如果为-1 的话表示创建失败。

        epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件, epoll_ctl 函数原型如下所示:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

        epfd:要操作的 epoll 句柄,也就是使用 epoll_create 函数创建的 epoll 句柄

        op:表示要对 epfd(epoll 句柄)进行的操作,可以设置为:

EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符。
EPOLL_CTL_MOD 修改参数 fd 的 event 事件。
EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。

         fd:要监视的文件描述符。

        event:要监视的事件类型,为 epoll_event 结构体类型指针, epoll_event 结构体类型如下所示:

struct epoll_event {
    uint32_t events; /* epoll 事件 */
    epoll_data_t data; /* 用户数据 */
};

         events: 表示要监视的事件,可选的事件如下所示:

EPOLLIN          有数据可以读取。
EPOLLOUT         可以写数据。
EPOLLPRI         有紧急的数据需要读取。
EPOLLERR         指定的文件描述符发生错误。
EPOLLHUP         指定的文件描述符挂起。
EPOLLET          设置 epoll 为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT     一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将
                 fd 重新添加到 epoll 里面

可以进行‘或’操作,耶尔就是可以设置监视多个事件

        返回值: 0,成功; -1,失败,并且设置 errno 的值为相应的错误码。

        一切都设置好以后应用程序就可以通过 epoll_wait 函数来等待事件的发生,类似 select 函数。 epoll_wait 函数原型如下所示:

int epoll_wait(int epfd, struct epoll_event *events, 
               int maxevents, int timeout)

        epfd: 要等待的 epoll

        events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events。调用者可以根据 events 判断发生了哪些事件。

        maxevents: events 数组大小,必须大于 0。

        timeout: 超时时间,单位为 ms。

        返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量。

注:epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当设计到的文件描述符(fd)比较少的时候就适合用 selcet 和 poll。

三、Linux驱动下的poll操作函数

        当应用程序调用 select poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数, poll 函数原型如下所示:

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

        filp: 要打开的设备文件(文件描述符)。

        wait:结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。

        返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:

POLLIN              有数据可以读取。
POLLPRI             有紧急的数据需要读取。
POLLOUT             可以写数据。
POLLERR             指定的文件描述符发生错误。
POLLHUP             指定的文件描述符挂起。
POLLNVAL            无效的请求。
POLLRDNORM          等同于 POLLIN,普通数据可读

        我们需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中, poll_wait 函数定义linux/poll.h里面原型如下:

static inline void poll_wait(struct file * filp, 
                             wait_queue_head_t * wait_address, 
                             poll_table *p)

        filp:就是poll函数中的filp要打开的设备文件(文件描述符)。

        wait_address:等待列表头。

        p:就是file_operations poll 函数的 wait 参数。

        当应用程序使用poll或select函数时驱动对应的poll函数示例:


/* 第一步
 * 第二步
 * 第三步
 *
 */

wait_queue_head_t     r_wait

/* 读操作 */
ssize_t xxx_read (struct file *filep, char __user *buf, size_t cnt, loff_t *plot)
{
    /* 判断阻塞读还是非阻塞读 */
    if(条件 & O_NONBLOCK) {  /* 非阻塞读 */
        if(读数据没读到){     /* 无效数据 */
            return -EAGAIN;
        }
    }else{                    /* 阻塞读 */
        wait_event_interruptible(r_wait, 条件);   /* 可被信号打断 */
    }
}

/* poll操作 */
unsigned int xxx_poll (struct file *filep, struct poll_table_struct *wait)
{
    int mask = 0;
    /* poll函数必须调用poll_wait函数 */
    poll_wait(filep, &dev->r_wait, wait);

    /* 是否可读 */
    if (读到数据的条件){     /* 按键按下,可读 */ 
        mask =  POLLIN | POLLRDNORM;
    }
		
    return mask;
}


/* 驱动入口函数 */
int __init xxx_init(void)
{

    init_waitqueue_head(&r_wait);
}

/* 中断处理函数 */
irqreturn_t xxx(int irq, void *dev_id)
{
    if(唤醒条件){
        wake_up(&r_wait);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tofu_Cabbage

你的打赏是我的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值