linux设备驱动 阻塞与非阻塞 I/O、异步通知与异步IO

1、阻塞与非阻塞 I/O

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。
非阻塞操作的进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止
       阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费 CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源“礼让”给其他进程。
       因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
 

1.1、等待队列

在 Linux 驱动程序中,可以使用等待队列( wait queue)来实现阻塞进程的唤醒。
1.定义“等待队列头”
wait_queue_head_t my_queue;
2.初始化“等待队列头”
init_waitqueue_head(&my_queue);
而下面的 DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列
DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为 name 的等待队列。
4.添加/移除等待队列
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

add_wait_queue()用于将等待队列 wait 添加到等待队列头 q 指向的等待队列链表中,而remove_wait_queue()用于将等待队列 wait 从附属的等待队列头 q 指向的等待队列链表中移除。
5.等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

等待第 1 个参数 queue 作为等待队列头的等待队列被唤醒,而且第 2 个参数 condition 必须满足,否则继续阻塞。

wait_event()和 wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout 后的宏意味着阻塞等待的超时时间,以 jiffy 为单位,在第 3 个参数的 timeout到达时,不论 condition 是否满足,均返回。
6.唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

wake_up()应该与 wait_event()或 wait_event_timeout()成对使用;

wake_up_interruptible()则应与 wait_event_interruptible()或 wait_event_interruptible_timeout()成对使用。

wake_up()可唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的进程,而 wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE 的进程。


说明:

TASK_INTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比方等待键盘输入、socket连接、信号等等),但能够被中断唤醒.普通情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态。
TASK_UNINTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比方等待键盘输入、socket连接、信号等等),但不能够被中断唤醒。

TASK_ZOMBIE:僵死状态。进程资源用户空间被释放,但内核中的进程PCB并没有释放。等待父进程回收。
TASK_STOPPED:进程被外部程序暂停(如收到SIGSTOP信号,进程会进入到TASK_STOPPED状态),当再次同意时继续运行(进程收到SIGCONT信号,进入TASK_RUNNING状态)。因此处于这一状态的进程能够被唤醒。


7.在等待队列上睡眠
sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );

sleep_on()函数应该与 wake_up()成对使用, interruptible_sleep_on()应该与 wake_up_interruptible()成对使用。

在内核中使用 set_current_state()函数或_ _add_current_state()函数来实现目前进程状态的改变,直接采用 current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句也是可行的。通常而言, set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于_ _add_current_state()。
因此,在许多设备驱动中,并不调用 sleep_on()或 interruptible_sleep_on(),而是亲自进行进程的状态改变和切换,如下代码

在驱动程序中改变进程状态并调用 schedule()

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count,
2 loff_t *ppos)
3 {
4 ...
5 DECLARE_WAITQUEUE(wait, current); /* 定义等待队列 */
6 add_wait_queue(&xxx_wait, &wait); /* 添加等待队列 */
7
8 ret = count;
9 /* 等待设备缓冲区可写 */
10 do {
11 avail = device_writable(...);
12 if (avail < 0)
13 _ _set_current_state(TASK_INTERRUPTIBLE);/* 改变进程状态 */
14
15 if (avail < 0) {
16 if (file->f_flags &O_NONBLOCK) {/* 非阻塞 */
17 if (!ret)
18 ret = - EAGAIN;
19 goto out;
20 }
21 schedule(); /* 调度其他进程执行
22 if (signal_pending(current)) {/* 如果是因为信号唤醒 */
23 if (!ret)
24 ret = - ERESTARTSYS;
25 goto out;
26 }
27 }
28 }while (avail < 0);
29
30 /* 写设备缓冲区 */
31 device_write(...)
32 out:
33 remove_wait_queue(&xxx_wait, &wait);/* 将等待队列移出等待队列头 */
34 set_current_state(TASK_RUNNING);/*设置进程状态为 TASK_RUNNING*/
35 return ret;
36 }

① 如果是非阻塞访问( O_NONBLOCK 被设置),设备忙时,直接返回“ -EAGAIN”。
② 对于阻塞访问,会进行状态切换并显式通过“ schedule()”调度其他进程执行;
③ 醒来的时候要注意,由于调度出去的时候,进程状态是 TASK_INTERRUPTIBLE,即浅度睡眠,因此唤醒它的有可能是信号,因此,我们首先通过“ signal_pending(current)”了解是不是信号唤醒的,如果是,立即返回“ - ERESTARTSYS”。
 

支持阻塞操作的 globalfifo 设备驱动

1 struct globalfifo_dev { 
2 struct cdev cdev; /*cdev 结构体*/
3 unsigned int current_len; /*fifo 有效数据长度*/
4 unsigned char mem[GLOBALFIFO_SIZE]; /*全局内存*/
5 struct semaphore sem; /*并发控制用的信号量*/
6 wait_queue_head_t r_wait; /*阻塞读用的等待队列头*/
7 wait_queue_head_t w_wait; /*阻塞写用的等待队列头*/
8 };

增加等待队列后的 globalfifo 读写函数
 

1 /*globalfifo 读函数*/
2 static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t
3 count, loff_t *ppos)
4 {
5 int ret;
6 struct globalfifo_dev *dev = filp->private_data; /* 获得设备结构体指针
7 DECLARE_WAITQUEUE(wait, current); /* 定义等待队列
8
9 down(&dev->sem); /* 获得信号量
10 add_wait_queue(&dev->r_wait, &wait); /* 进入读等待队列头
11
12 /* 等待 FIFO 非空 */
13 while (dev->current_len == 0) {
14 if (filp->f_flags &O_NONBLOCK) {
15 ret = - EAGAIN;
16 goto out;
17 }
18 __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠
19 up(&dev->sem);
20
21 schedule(); /* 调度其他进程执行
22 if (signal_pending(current)) {/* 如果是因为信号唤醒 */
23 ret = - ERESTARTSYS;
24 goto out2;
25 }
26
27 down(&dev->sem);
28 }
29
30 /* 拷贝到用户空间 */
31 if (count > dev->current_len)
32 count = dev->current_len;
33
34 if (copy_to_user(buf, dev->mem, count)) {
35 ret = - EFAULT;
36 goto out;
37 } else {
38 memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo 数据前移*/
39 dev->current_len -= count; /* 有效数据长度减少
40 printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev
41 ->current_len);
42
43 wake_up_interruptible(&dev->w_wait); /* 唤醒写等待队列*/
44
45 ret = count;
46 }
47 out: up(&dev->sem); /* 释放信号量
48 out2: remove_wait_queue(&dev->w_wait, &wait); /* 移除等待队列*/
49 set_current_state(TASK_RUNNING);
50 return ret;
51 }
52
53///////////////////////////////////////////////////////////////////////////////////
54 /*globalfifo 写操作*/
55 static ssize_t globalfifo_write(struct file *filp, const char _ _user *buf,
56 size_t count, loff_t *ppos)
57 {
58 struct globalfifo_dev *dev = filp->private_data; /* 获得设备结构体指针*/
59 int ret;
60 DECLARE_WAITQUEUE(wait, current); /* 定义等待队列*/
61
62 down(&dev->sem); /* 获取信号量*/
63 add_wait_queue(&dev->w_wait, &wait); /* 进入写等待队列头*/
64
65 /* 等待 FIFO 非满 */
66 while (dev->current_len == GLOBALFIFO_SIZE) {
67 if (filp->f_flags &O_NONBLOCK) {
68 /* 如果是非阻塞访问*/
69 ret = - EAGAIN;
70 goto out;
71 }
72
__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠*/
73 up(&dev->sem);
74
75 schedule(); /* 调度其他进程执行*/
76 if (signal_pending(current)) {
77 /* 如果是因为信号唤醒*/
78 ret = - ERESTARTSYS;
79 goto out2;
80 }
81
82 down(&dev->sem); /* 获得信号量 */
83 }
84
85 /*从用户空间拷贝到内核空间*/
86 if (count > GLOBALFIFO_SIZE - dev->current_len)
87 count = GLOBALFIFO_SIZE - dev->current_len;
88
89 if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
90 ret = - EFAULT;
91 goto out;
92 } else {
93 dev->current_len += count;
94 printk(KERN_INFO "written %d bytes(s),current_len:%d\n", count, dev
95 ->current_len);
96
97 wake_up_interruptible(&dev->r_wait); /* 唤醒读等待队列 */
98
99 ret = count;
100 }
101
102 out: up(&dev->sem); /* 释放信号量 */
103 out2: remove_wait_queue(&dev->w_wait, &wait);
104 set_current_state(TASK_RUNNING);
105 return ret;
106 }

细微的区别体现在第 13~28 行代码和第 66~83 行代码在进行 schedule()即切换进程前,通过 up(&dev->sem)释放了信号量。这一细微的动作意义重大,非如此,则死锁将不可避免。

linux设备驱动 阻塞与非阻塞 I/O、异步通知与异步IO_第1张图片

linux设备驱动 阻塞与非阻塞 I/O、异步通知与异步IO_第2张图片

 

1.2轮询操作

使用非阻塞 I/O的应用程序通常会使用 select()和 poll()系统调用查询是否可对设备进行无阻塞的访问。 select()和
poll()系统调用最终会引发设备驱动中的 poll()函数被执行。
a)应用程序中的轮询编程
应用程序中最广泛用到的是 BSD UNIX 中引入的 select()系统调用,其原型为:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中 readfds、 writefds、 exceptfds 分别是被 select()监视的读、写和异常处理的文件描述符集合, numfds 的值是需要检查的号码最高的文件描述符加 1。 timeout 参数是一个指向 struct timeval类型的指针,它可以使 select()在等待 timeout 时间后若没有文件描述符准备好则返回。 struct timeval数据结构的定义如代码清单所示。

//代码清单  timeval 结构体定义
1 struct timeval {
2 int tv_sec; /* 秒 */
3 int tv_usec; /* 微秒 */
4 };
下列操作用来设置、清除、判断文件描述符集合:
FD_ZERO(fd_set *set)
清除一个文件描述符集;
FD_SET(int fd,fd_set *set)
将一个文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)
将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)
判断文件描述符是否被置位。

b)设备驱动中的轮询编程

设备驱动中 poll()函数的原型是:
unsigned int(*poll)(struct file * filp, struct poll_table* wait);
第 1 个参数为 file 结构体指针,第 2 个参数为轮询表指针。这个函数应该进行两项工作。
( 1)对可能引起设备文件状态变化的等待队列调用 poll_wait()函数,将对应的等待队列头添
加到 poll_table。

( 2)返回表示是否能对设备进行无阻塞读、写访问的掩码。
关键的用于向 poll_table 注册等待队列的 poll_wait()函数的原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);
poll_wait()函数的名称非常容易让人产生误会,以为它和 wait_event()等一样,会阻塞地等待某事件的发生,其实这个函数并不会引起阻塞。 poll_wait()函数所做的工作是把当前进程添加到wait 参数指定的等待列表( poll_table)中。

poll()函数典型模板:

1 static unsigned int xxx_poll(struct file *filp, poll_table *wait)
2 {
3 unsigned int mask = 0;
4 struct xxx_dev *dev = filp->private_data; /*获得设备结构体指针*/
5
6 ...
7 poll_wait(filp, &dev->r_wait, wait);/* 加读等待队列头 */
8 poll_wait(filp, &dev->w_wait, wait);/* 加写等待队列头 */
9
10 if (...) /* 可读 */
11 mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
12
13 if (...) /* 可写 */
14 mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
15 ...
16 return mask;
17 }

globalfifo 设备驱动的 poll()函数

static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
  unsigned int mask = 0;
  struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/
  
  down(&dev->sem);
  
  poll_wait(filp, &dev->r_wait, wait);
  poll_wait(filp, &dev->w_wait, wait);  
  /*fifo非空*/
  if (dev->current_len != 0)
  {
    mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
  }
  /*fifo非满*/
  if (dev->current_len != GLOBALFIFO_SIZE)
  {
    mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
  }
     
  up(&dev->sem);
  return mask;
}

监控 globalfifo 是否可非阻塞读写的应用程序

#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
main()
{
  int fd, num;
  char rd_ch[BUFFER_LEN];
  fd_set rfds,wfds;
  
  /*以非阻塞方式打开/dev/globalmem设备文件*/
  fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
  if (fd !=  - 1)
  {
    /*FIFO清0*/
    if (ioctl(fd, FIFO_CLEAR, 0) < 0)
    {
      printf("ioctl command failed\n");
    }
    while (1)
    {
      FD_ZERO(&rfds);
      FD_ZERO(&wfds);
      FD_SET(fd, &rfds);
      FD_SET(fd, &wfds);

      select(fd + 1, &rfds, &wfds, NULL, NULL);
      /*数据可获得*/
      if (FD_ISSET(fd, &rfds))
      {
      	printf("Poll monitor:can be read\n");
      }
      /*数据可写入*/
      if (FD_ISSET(fd, &wfds))
      {
      	printf("Poll monitor:can be written\n");
      }      
    }
  }
  else
  {
    printf("Device open failure\n");
  }
}

1.3异步通知

       异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步 I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
      阻塞 I/O 意味着一直等待设备可访问后再访问,非阻塞 I/O 中使用 poll()意味着查询设备是否可访问,而异步通知则意味着设备通知自身可访问,实现了异步 I/O。

使用信号进行进程间通信( IPC)是 UNIX 中的一种传统机制, Linux 也支持这种机制。在Linux 中,异步通知使用信号来实现。

a)信号的接收
在用户程序中,为了捕获信号,可以使用 signal()函数来设置对应信号的处理函数:
void (*signal(int signum, void (*handler))(int)))(int);
该函数原型较难理解,它可以分解为:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理函数,若为 SIG_IGN, 表示忽略该信号;若为 SIG_DFL,表示采用系统默认方式处理信号;若为用户自定义的函数,则信号被捕获到后,该函数将被执行。
如果 signal()调用成功,它返回最后一次为信号 signum 绑定的处理函数 handler 值,失败则返回 SIG_ERR。
b)处理 FASYNC 标志变更的,从进程列表中增加或者删除文件。
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
c)通知所有相关进程。
void kill_fasync(struct fasync_struct **fa, int sig, int band);

sig就是我们要发送的信号。

band(带宽),一般都是使用POLL_IN,表示设备可读,如果设备可写,使用POLL_OUT

d)将文件从异步通知列表中删除 */
 globalfifo_fasync( - 1, filp, 0);

使用信号实现异步通知的应用程序实例:

7 #define MAX_LEN 100
8 void input_handler(int num)
9 {
10 char data[MAX_LEN];
11 int len;
12
13 /* 读取并输出 STDIN_FILENO 上的输入 */
14 len = read(STDIN_FILENO, &data, MAX_LEN);
15 data[len] = 0;
16 printf("input available:%s\n", data);
17 }
18
19 main()
20 {
21 int oflags;
22
23 /* 启动信号驱动机制 */
24 signal(SIGIO, input_handler);
25 fcntl(STDIN_FILENO, F_SETOWN, getpid());
26 oflags = fcntl(STDIN_FILENO, F_GETFL);
27 fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
28
29 /* 最后进入一个死循环,仅为保持进程不终止,如果程序中
30 没有这个死循会立即执行完毕 */
31 while (1);
32 }

上述代码 24 行为 SIGIO 信号安装 input_handler()作为处理函数,第 25 行设置本进程为STDIN_FILENO 文件的拥有者( owner),没有这一步内核不会知道应该将信号发给哪个进程。而为了启用异步通知机制,还需对设备设置 FASYNC 标志, 26~27 行代码实现此目的。
 

支持异步通知的 globalfifo 设备驱动写函数:

1 static int globalfifo_fasync(int fd, struct file *filp, int mode)
2 {
3 struct globalfifo_dev *dev = filp->private_data;
4 return fasync_helper(fd, filp, mode, &dev->async_queue);
5 }

1 static ssize_t globalfifo_write(struct file *filp, const char __user *buf,
2 size_t count, loff_t *ppos)
3 {
4 struct globalfifo_dev *dev = filp->private_data; /* 获得设备结构体指针 */
5 int ret;
6 DECLARE_WAITQUEUE(wait, current); /*定义等待队列*/
7
8 down(&dev->sem); /*获取信号量*/
9 add_wait_queue(&dev->w_wait, &wait); /*进入写等待队列头*/
10
11 /* 等待 FIFO 非满 */
12 if (dev->current_len == GLOBALFIFO_SIZE) {
13 if (filp->f_flags &O_NONBLOCK) { /* 如果是非阻塞访问 */
14 ret = - EAGAIN;
15 goto out;
16 }
17
__set_current_state(TASK_INTERRUPTIBLE); /*改变进程状态为睡眠*/
18 up(&dev->sem);
19
20 schedule(); /*调度其他进程执行*/
21 if (signal_pending(current)) { /* 如果是因为信号唤醒 */
22 ret = - ERESTARTSYS;
23 goto out2;
24 }
25
26 down(&dev->sem); /*获得信号量*/
27 }
28
29 /*从用户空间拷贝到内核空间*/
30 if (count > GLOBALFIFO_SIZE - dev->current_len)
31 count = GLOBALFIFO_SIZE - dev->current_len;
32
33 if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
34 ret = - EFAULT;
35 goto out;
36 } else {
37 dev->current_len += count;
38 printk(KERN_INFO "written %d bytes(s),current_len:%d\n", count, dev
39 ->current_len);
40
41 wake_up_interruptible(&dev->r_wait); /*唤醒读等待队列*/
42 /* 产生异步读信号 */
43 if (dev->async_queue)
44 kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
45
46 ret = count;
47 }
48
49 out: up(&dev->sem); /* 释放信号量 */
50 out2:remove_wait_queue(&dev->w_wait, &wait);
51 set_current_state(TASK_RUNNING);
52 return ret;
53 }


1 int globalfifo_release(struct inode *inode, struct file *filp)
2 {
3 /* 将文件从异步通知列表中删除 */
4 globalfifo_fasync( - 1, filp, 0);
5 return 0;
6 }

监控 globalfifo 异步通知信号的应用程序

/*接收到异步读信号后的动作*/
4 void input_handler(int signum)
5 {
6 printf("receive a signal from globalfifo,signalnum:%d\n",signum);
7 }
8
9 main()
10 {
11 int fd, oflags;
12 fd = open("/dev/globalfifo", O_RDWR, S_IRUSR | S_IWUSR);
13 if (fd != - 1) {
14 /* 启动信号驱动机制 */
15 signal(SIGIO, input_handler); /* 让 input_handler()处理 SIGIO 信号 */
16 fcntl(fd, F_SETOWN, getpid());
17 oflags = fcntl(fd, F_GETFL);
18 fcntl(fd, F_SETFL, oflags | FASYNC);
19 while(1) {
20 sleep(100);
21 }
22 } else {
23  printf("device open failure\n");
24 }
25 }

1.4 异步IO

        AIO(异步IO) 基本思想是允许进程发起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完成的通知时,进程再检索 I/O 操作的结果。
        select()函数所提供的功能(异步阻塞 I/O)与 AIO 类似,它对通知事件进行阻塞,而不是对I/O 调用进行阻塞。
        在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,通过 aiocb( AIO I/O Control Block)结构体进行区分。这个结构体包含了有关传输的所有信息, 以及为数据准备的用户缓冲区。在产生 I/O 通知(称为完成)时, aiocb 结构就被用来惟一标识所完成的 I/O 操作。
未完待续。。。。

 

 

 

你可能感兴趣的:(linux驱动)