LV10-03-IO模型-02-IO模型在驱动层的实现-01-阻塞IO

本文主要是IO模型在驱动层的实现中的阻塞IO的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、阻塞IO

1. 应用层

应用层主要是对字符设备的打开、读写等,应用层实现阻塞,主要是通过 open() 函数打开的时候设置阻塞或者非阻塞,或者就是打开之后使用fcntl()函数来更改阻塞状态。

1.1 open()

1.1.1 函数说明

在linux下可以使用man 2 open命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/* 函数声明 */
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

【函数说明】该函数打开一个文件(在本篇中就是打开设备节点),获取文件描述符;打开文件时使用两个参数,创建文件时第三个参数指定新文件的权限。

【函数参数】

  • pathname :char *类型,为被打开的文件名(可包括路径名)。
  • flags :int类型,表示打开文件所采用的操作。
点击查看详细的 flag 常量
  • flag 常量可取的值
O_RDONLY只读方式打开文件。这三个参数互斥
O_WRONLY可写方式打开文件。
O_RDWR 读写方式打开文件。
O_CREAT 如果该文件不存在,就创建一个新的文件,并用第三的参数为其设置权限。
O_EXCL 如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。
O_NOCTTY使用本参数时,如文件为终端,那么终端不可以作为调用open()系统调用的那个进程的控制终端。
O_TRUNC 如文件已经存在,那么打开文件时先删除文件中原有数据。
O_APPEND以添加方式打开文件,所以对文件的写操作都在文件的末尾进行。

【注意事项】前三个参数必须指定,且只能指定一个,后边的几个可以与前边搭配使用。

  • flag 常量与标准I/O文件打开权限关系、
r O_RDONLY
r+O_RDWR
w O_WRONLY | O_CREAT | O_TRUNC, 0664
w+O_RDWR | O_CREAT | O_TRUNC, 0664
a O_WRONLY | O_CREAT | O_APPEND, 0664
a+O_RDWR | O_CREAT | O_APPEND, 0664
  • mode :mode_t 类型,表示被打开文件的存取权限,为8进制表示法。此参数只有在建立新文件时有效。新建文件时的权限会受到umask 值影响,实际权限是mode - umaks。
点击查看什么是 umask

在类unix系统中,umask是确定掩码设置的命令,该掩码用来设定文件或目录的初始权限。umask确定了文件创建时的初始权限:

1
文件或目录的初始权限 = 文件或目录的最大默认权限 - umask权限

文件初始默认权限为0666,目录为0777,若用户umask为0002,则新创建的文件或目录在没有指定的情况下默认权限分别为0664、0775)。

在Linux下,我们可以使用umask命令来查看当前用户默认的umask值,同时也可以在umask命令后面跟上需要设置的umask值来重新设置umask。

【返回值】int 类型,成功时返回文件描述符(非负整数);出错时返回EOF。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/* 至少应该有的语句 */
int fd;/* 接收文件描述符 */
/* 以只写的方式打开设备文件 /dev/newchar_dev */
fd = open("/dev/newchar_dev", O_WRONLY);

【注意事项】该函数可以打开设备文件,但是不能创建设备文件

1.1.2 使用实例

暂无

1.2 fcntl()

1.2.1 函数说明

在linux下可以使用man 2 fcntl命令查看该函数的帮助手册。

1
2
3
4
5
6
/* 需包含的头文件 */
#include <unistd.h>
#include <fcntl.h>

/* 函数声明 */
int fcntl(int fd, int cmd, ... /* arg */ );

【函数说明】该函数是一个系统调用,可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。

【函数参数】

  • fd :int 类型,已打开的文件描述符。
  • cmd :int类型,表示要对文件描述符采取的操作,我们常用的就是F_GETFL和F_SETFL,常用于改变已打开文件的阻塞状态,不过还有很多其他命令,例如文件锁,详细的可以查看 man 手册。
  • arg :可选参数,由cmd决定是否需要该参数,比如 cmd 为F_GETFL的时候就不需要第三个参数,cmd为F_SETFL的时候是需要有第三个参数的。

【返回值】int 类型,不同的cmd,返回值的含义不同,详情可以查看man手册,里边有很详细的说明。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <unistd.h>
#include <fcntl.h>

/* 至少应该有的语句 */
flags = fcntl(fd,F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags); // 将open改为非阻塞

【注意事项】none

1.2.2 使用实例

暂无

2. 驱动层

2.1 相关结构体

2.1.1 wait_queue_head_t

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。

Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在linux内核源码的这个文件中:

1
include/linux/wait.h

我们打开这个文件,结构体成员如下所示:

1
2
3
4
5
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

2.1.2 wait_queue_t

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项 ,它定义在linux内核源码的这个文件中:

1
include/linux/wait.h

我们打开这个文件,结构体成员如下所示:

1
2
3
4
5
6
7
8
9
typedef struct __wait_queue wait_queue_t;

struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};

2.2 相关函数

2.2.1 init_waitqueue_head()

2.2.1.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep init_waitqueue_head -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 需包含的头文件 */
#include <linux/wait.h>

/* 函数定义 */
extern void __init_waitqueue_head(wait_queue_head_t *q, const char *name, struct lock_class_key *);

#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 *pwq);//初始化等待队列头

【函数说明】该函数用于初始化等待队列的头。

【函数参数】

  • pwq :wait_queue_head_t * 类型,表示要初始化的等待队列头。

【返回值】void * 类型,返回分配好的内存的首地址,一般都需要进行强制类型转换,转换为我们需要的类型,分配失败返回NULL。

【使用格式】

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <linux/wait.h>

/* 至少要有的语句 */
wait_queue_head_t rq; /* 读等待队列头数据类型 */
wait_queue_head_t wq; /* 读等待队列头数据类型 */
init_waitqueue_head(&rq); /* 初始化等待队列头 */
init_waitqueue_head(&wq); /* 初始化等待队列头 */

【注意事项】 none

2.2.1.2 使用实例

暂无

2.2.2 wake_up ()

2.2.2.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep wake_up -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <linux/wait.h>

/* 函数定义 */
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
// 其实它是这个样子的
void wake_up(wait_queue_head_t *q);

【函数说明】该函数其实是经过封装的宏,用于唤醒等待队列头中的所有进程(主动唤醒进程)。

【函数参数】

  • x :wait_queue_head_t * 类型,表示要唤醒的等待队列头。

【返回值】none

【使用格式】none

【注意事项】 wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程。

2.2.2.2 使用实例

暂无

2.2.3 wake_up_interruptible ()

2.2.3.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep wake_up_interruptible -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <linux/wait.h>

/* 函数定义 */
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
// 其实它是这个样子的
void wake_up_interruptible(wait_queue_head_t *q)

【函数说明】该函数其实是经过封装的宏,与wake_up() 类似,用于唤醒等待队列头中的所有进程(主动唤醒进程)。

【函数参数】

  • x :wait_queue_head_t * 类型,表示要唤醒的等待队列头。

【返回值】none

【使用格式】none

【注意事项】 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

2.2.3.2 使用实例

暂无

2.2.4 wait_event ()

2.2.4.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep wait_event -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
/* 需包含的头文件 */
#include <linux/wait.h>

/* 函数定义 */
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)

【函数说明】该函数其实是经过封装的宏,当某种条件被满足的时候自动唤醒等待队列中的进程。

【函数参数】

  • wq :wait_queue_head_t * 类型,表示要唤醒的等待队列头。
  • condition :条件表达式,为真时以wq为等待队列头的等待队列被唤醒,否则一直阻塞。

【返回值】none

【使用格式】none

【注意事项】 此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态。

2.2.4.2 使用实例

暂无

2.2.5 wait_event_interruptible ()

2.2.5.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep wait_event_interruptible -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
/* 需包含的头文件 */
#include <linux/wait.h>

/* 函数定义 */
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0; \
if (!(condition)) \
__ret = __wait_event_interruptible(wq, condition); \
__ret; \
})

【函数说明】该函数其实是经过封装的宏,功能和 wait_event 类似,也是当某种条件被满足的时候自动唤醒等待队列中的进程,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打 断。

【函数参数】

  • wq :wait_queue_head_t * 类型,表示要唤醒的等待队列头。
  • condition :条件表达式,为真时以wq为等待队列头的等待队列被唤醒,否则一直阻塞。

【返回值】none

【使用格式】none

【注意事项】 此函数会将进程设置为TASK_INTERRUPTIBLE状态。

2.2.5.2 使用实例

暂无

3. 使用步骤

编写的驱动支持阻塞IO的话,一般步骤如下:

  • (1)用wait_queue_head_t数据类型定义等待队列头,注意读写使用不同的等待队列头,也就是需要定义两个等待队列头;
  • (2)用 init_waitqueue_head() 函数初始化等待队列头;
  • (3)驱动中的read()函数判断是否支持阻塞IO,若支持,则调用 wake_up_interruptible() 函数阻塞,当条件满足时读取数据,否则直接读取数据。数据读取完毕后,调用wake_up_interruptible()函数唤醒需要写的进程。
  • (4)驱动中的write()函数判断是否支持阻塞IO,若支持,则调用 wake_up_interruptible() 函数阻塞,当条件满足时写入数据,否则直接写入数据。数据写入完毕后,调用wake_up_interruptible()函数唤醒需要读的进程。