Linux 阻塞和非阻塞 IO 实验学习

news2024/11/17 5:49:48

Linux 阻塞和非阻塞 IO 实验学习

IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式 IO 如图 52.1.1.1所示:
在这里插入图片描述
应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
非阻塞 IO 如图 52.1.2 所示:
在这里插入图片描述
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。

示例代码 52.1.1.1 应用程序阻塞读取数据
1 int fd;
2 int data = 0;
3 4
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */

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

示例代码 52.1.1.2 应用程序非阻塞读取数据
1 int fd;
2 int data = 0;
3 4
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */4 行使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,
表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。

等待队列

1、等待队列头

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

示例代码 52.1.2.1 wait_queue_head_t 结构体
39 struct __wait_queue_head {
40 spinlock_t lock;
41 struct list_head task_list;
42 };
43 typedef struct __wait_queue_head wait_queue_head_t;

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

void init_waitqueue_head(wait_queue_head_t *q)

参数 q 就是要初始化的等待队列头。也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。

2、等待队列项

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

示例代码 52.1.2.2 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;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:DECLARE_WAITQUEUE(name, tsk),name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

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

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

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函数参数和返回值含义如下:
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。

等待队列项移除 API 函数如下:

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函数参数和返回值含义如下:
q: 要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。
4、等待唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:

void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)

参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

5、等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的 API 函数如表 52.1.2.1 所示:
在这里插入图片描述
在这里插入图片描述

轮询

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

1、 select 函数

select 函数原型如下:

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

nfds: 所要监视的这三类文件描述集合中, 最大文件描述符加 1。
readfds、 writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的, fd_set 类型变量的每一个位都代表了一个文件描述符。readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。 writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。 exceptfds 用于监视这些文件的异常。
我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

FD_ZERO 用于将 fd_set 变量的所有位都清零, FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。 FD_CLR 用于将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。 FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:

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

当 timeout 为 NULL 的时候就表示无限期的等待。
返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生
错误;其他值,可以进行操作的文件描述符个数。
使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

示例代码 52.1.3.1 select 函数非阻塞读访问示例
1 void main(void)
2 {
3 int ret, fd; /* 要监视的文件描述符 */
4 fd_set readfds; /* 读操作文件描述符集 */
5 struct timeval timeout; /* 超时结构体 */
6 7
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
8 9
FD_ZERO(&readfds); /* 清除 readfds */
10 FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */
11
12 /* 构造超时时间 */
13 timeout.tv_sec = 0;
14 timeout.tv_usec = 500000; /* 500ms */
15
16 ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
17 switch (ret) {
18 case 0: /* 超时 */
19 printf("timeout!\r\n");
20 break;
21 case -1: /* 错误 */
22 printf("error!\r\n");
23 break;
24 default: /* 可以读取数据 */
25 if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
26 /* 使用 read 函数读取数据 */
27 }
28 break;
29 }
30 }
2、 poll 函数

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

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。 events 是要监视的事件,可监视的事件类型如下所示:

POLLIN 有数据可以读取
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
revents 是返回参数,也就是返回的事件, 由 Linux 内核设置具体的返回事件。
nfds: poll 函数要监视的文件描述符数量。
timeout: 超时时间,单位为 ms。
返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述
符数量; 0,超时; -1,发生错误,并且设置 errno 为错误类型。

使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

示例代码 52.1.3.2 poll 函数读非阻塞访问示例
1 void main(void)
2 {
3 int ret;
4 int fd; /* 要监视的文件描述符 */
5 struct pollfd fds;
6 7
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
8 9
/* 构造结构体 */
10 fds.fd = fd;
11 fds.events = POLLIN; /* 监视数据是否可以读取 */
12
13 ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
14 if (ret) { /* 数据有效 */
15 ......
16 /* 读取数据 */
17 ......
18 } else if (ret == 0) { /* 超时 */
19 ......
20 } else if (ret < 0) { /* 错误 */
21 ......
22 }
23 }
3、 epoll 函数

传统的 selcet 和 poll 函数都会随着所监听的 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; /* 用户数据 */
};
结构体 epoll_event 的 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,本章我们就使用 sellect 和 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 函数原型如下:

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

参数 wait_address 是要添加到 poll_table 中的等待队列头,参数 p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数。

阻塞 IO 实验

在这里插入图片描述
imx6uirqApp 这个应用程序的 CPU 使用率竟然高达 99.6%,这仅仅是一个读取按键值的应用程序,这么高的 CPU 使用率显然是有问题的!原因就在于我们是直接在 while 循环中通过 read 函数读取按键值,因此imx6uirqApp 这个软件会一直运行,一直读取按键值, CPU 使用率肯定就会很高。最好的方法就是在没有有效的按键事件发生的时候,imx6uirqApp 这个应用程序应该处于休眠状态,当有按键事件发生以后 imx6uirqApp 这个应用程序才运行,打印出按键值,这样就会降低 CPU 使用率,
下面开始阻塞实验的编写:
首先设备结构体的编写,里面新增队列头变量。

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			/* 设备号 	 */	
	struct cdev cdev;		/* cdev 	*/                 
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */	
	atomic_t keyvalue;		/* 有效的按键键值 */
	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
	struct timer_list timer;/* 定义一个定时器*/
	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键init述数组 */
	unsigned char curkeynum;				/* 当前init按键号 */

	wait_queue_head_t r_wait;	/* 读等待队列头 */
};

如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,所以在设备结构体信息添加这个变量:wait_queue_head_t r_wait
接下来我们要考虑初始化这个队列头啥的。

static int keyio_init(void)
{
	unsigned char i = 0;
	char name[10];
	int ret = 0;
	
	imx6uirq.nd = of_find_node_by_path("/key");
	if (imx6uirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} 

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
		if (imx6uirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(imx6uirq.irqkeydesc[i].gpio, name);
		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif
	}

	/* 申请中断 */
	imx6uirq.irqkeydesc[0].handler = key0_handler;
	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
     init_timer(&imx6uirq.timer);
     imx6uirq.timer.function = timer_function;

	/* 初始化等待队列头 */
	init_waitqueue_head(&imx6uirq.r_wait);
	return 0;
}

主要添加内容如上面代码所示,也就是这一句init_waitqueue_head(&imx6uirq.r_wait);
接下来一个重要的函数就是读函数。读函数如下列所示:

static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char releasekey = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

#if 0
	/* 加入等待队列,等待被唤醒,也就是有按键按下 */
 	ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
	if (ret) {
		goto wait_error;
	} 
#endif

	DECLARE_WAITQUEUE(wait, current);	/* 定义一个等待队列 */
	if(atomic_read(&dev->releasekey) == 0) {	/* 没有按键按下 */
		add_wait_queue(&dev->r_wait, &wait);	/* 将等待队列添加到等待队列头 */
		__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
		schedule();							/* 进行一次任务切换 */
		if(signal_pending(current))	{			/* 判断是否为信号引起的唤醒 */
			ret = -ERESTARTSYS;
			goto wait_error;
		}
		__set_current_state(TASK_RUNNING);      /* 将当前任务设置为运行状态 */
	    remove_wait_queue(&dev->r_wait, &wait);    /* 将对应的队列项从等待队列头删除 */
	}

	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		} else {
			goto data_error;
		}
		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
	} else {
		goto data_error;
	}
	return 0;

wait_error:
	set_current_state(TASK_RUNNING);		/* 设置任务为运行态 */
	remove_wait_queue(&dev->r_wait, &wait);	/* 将等待队列移除 */
	return ret;

data_error:
	return -EINVAL;
}

其中核心逻辑重要的地方如下:

DECLARE_WAITQUEUE(wait, current);	/* 定义一个等待队列 */
	if(atomic_read(&dev->releasekey) == 0) {	/* 没有按键按下 */
		add_wait_queue(&dev->r_wait, &wait);	/* 将等待队列添加到等待队列头 */
		__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
		schedule();							/* 进行一次任务切换 */
		if(signal_pending(current))	{			/* 判断是否为信号引起的唤醒 */
			ret = -ERESTARTSYS;
			goto wait_error;
		}
		__set_current_state(TASK_RUNNING);      /* 将当前任务设置为运行状态 */
	    remove_wait_queue(&dev->r_wait, &wait);    /* 将对应的队列项从等待队列头删除 */
	}
	wait_error:
	set_current_state(TASK_RUNNING);		/* 设置任务为运行态 */
	remove_wait_queue(&dev->r_wait, &wait);	/* 将等待队列移除 */
	return ret;

在Linux内核中,进程或线程可能需要等待某些条件满足才能继续执行。这种情况在设备驱动程序中尤其常见,比如等待硬件事件(如按键输入)。在这个代码段中,使用了一系列标准的内核函数和宏来实现进程的阻塞和唤醒,这些操作是为了安全和有效地处理进程等待和唤醒,具体包括以下几个步骤:

  1. __set_current_state(TASK_INTERRUPTIBLE);
    这行代码将当前进程的状态设置为TASK_INTERRUPTIBLE,即可中断的睡眠状态。在这种状态下,进程不会运行,从而不会消耗CPU资源,但它可以被信号等中断唤醒。这是一种资源节约和高效的等待方式。

  2. schedule();
    schedule()函数是进程调度器的核心功能,它会停止当前进程的执行,并选择另一个进程继续执行。当当前进程调用schedule()后,它会在调度器找到下一个要运行的进程之前进入睡眠状态,直到被唤醒。在我们的场景中,这意味着进程将等待直到按键事件发生(即等待队列被唤醒)。

  3. if(signal_pending(current))
    在从schedule()返回后,这行代码检查是否有未处理的信号需要当前进程处理。在Linux中,进程可以通过信号与其他进程交互,这些信号可以是用户通过键盘产生的(如Ctrl-C),也可以是其他进程或系统发送的。如果存在待处理的信号,这意味着进程被一个外部事件(而非按键事件)唤醒,这通常需要特殊处理。
    特殊处理主要是以下代码:

  4. -ERESTARTSYS
    如果检测到信号,函数返回-ERESTARTSYS。这个特殊的错误码告诉内核如果可能的话应该重新启动系统调用。这是对信号的一种响应方式,允许用户空间程序优雅地处理中断。

  5. __set_current_state(TASK_RUNNING);
    在确认不是由于信号唤醒后,将进程状态设置回TASK_RUNNING,表示进程现在是活跃的,并准备继续执行。

  6. remove_wait_queue(&dev->r_wait, &wait);
    最后,从等待队列中移除当前进程。这是必要的清理步骤,以确保等待队列只包含仍需要等待的进程。

接下来就是定时回调函数,因为我们是按键按下经过定时器延时消抖。所以唤醒任务进程应该定义在定时器回调函数中。

void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */
	}               

	/* 唤醒进程 */
	if(atomic_read(&dev->releasekey)) {	/* 完成一次按键过程 */
		/* wake_up(&dev->r_wait); */
		wake_up_interruptible(&dev->r_wait);
	}
}

测试APP与上一节内容基本类似。

非阻塞IO实验

非阻塞IO实验只需要在阻塞IO实验的基础上稍微修改即可。需要再设备读函数中添加:

if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞访问 */
	if(atomic_read(&dev->releasekey) == 0)	/* 没有按键按下,返回-EAGAIN */
		return -EAGAIN;
} else {							/* 阻塞访问 */
	/* 加入等待队列,等待被唤醒,也就是有按键按下 */
	ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
	if (ret) {
		goto wait_error;
	}
}

与上面阻塞实验编写的阻塞函数略有不同,这里用到的是等待事件函数。
在这里插入图片描述
在这里插入图片描述

区别分析:

让我们更详细地对比一下手动实现等待和唤醒的代码段与使用 wait_event_interruptible() 的差异。通过对每个步骤的详细分析,我们可以更清楚地看到两种方法的优缺点。

手动实现等待和唤醒

手动实现的代码段如下:

if (atomic_read(&dev->releasekey) == 0) {
    add_wait_queue(&dev->r_wait, &wait);
    __set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    if (signal_pending(current)) {
        ret = -ERESTARTSYS;
        goto wait_error;
    }
}
详细步骤分析
  1. 添加到等待队列

    • 使用 add_wait_queue() 将当前进程添加到 dev->r_wait 等待队列。这需要手动创建和初始化等待队列元素 wait
  2. 设置进程状态

    • 使用 __set_current_state(TASK_INTERRUPTIBLE) 直接设置进程的状态。这种方式较为低级,直接操作进程状态,可能导致竞态条件,如果在设置状态和调用 schedule() 之间发生中断,可能会错过唤醒事件。
  3. 调度其他任务

    • 调用 schedule() 让出CPU,使当前进程挂起,直到被唤醒。
  4. 检查唤醒原因

    • 检查是否由信号唤醒,如果是,则设置返回值 -ERESTARTSYS 并跳转到错误处理代码。
缺点
  • 复杂性:需要手动管理等待队列和进程状态,容易出错。
  • 竞态条件:在设置进程状态和调度之间可能发生竞态条件。
  • 错误处理:需要手动编写代码来处理信号唤醒的情况。
使用 wait_event_interruptible() 的方法

wait_event_interruptible() 的典型用法如下:

if (wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey) != 0)) {
    return -ERESTARTSYS;
}
详细步骤分析
  1. 等待条件

    • wait_event_interruptible() 将当前进程添加到等待队列,并在给定的条件(这里是 atomic_read(&dev->releasekey) != 0)不满足时进入休眠。只有当条件满足或接收到信号时才会返回。
  2. 自动处理进程状态和调度

    • 这个函数内部自动设置进程状态为 TASK_INTERRUPTIBLE 并在必要时调用 schedule()。这减少了竞态条件的风险。
  3. 自动错误处理

    • 如果由于信号而唤醒,函数返回非零值,通常是 -ERESTARTSYS,这简化了错误处理代码。

接下来就是非阻塞要用的poll函数。

 /*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按键按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}

接下来也要在设备操作函数中关联poll函数。

/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.poll = imx6uirq_poll,
};

接下来就是编写测试APP函数。我们需要在测试APP中用poll函数去反复查询设备是否可用。

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"
#include "linux/ioctl.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: noblockApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: 非阻塞访问测试APP
其他	   	: 无
使用方法	:./blockApp /dev/blockio 打开测试App
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/9/8 左忠凯创建
***************************************************************/

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd;
	int ret = 0;
	char *filename;
	struct pollfd fds;
	fd_set readfds;
	struct timeval timeout;
	unsigned char data;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR | O_NONBLOCK);	/* 非阻塞访问 */
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

#if 0
	/* 构造结构体 */
	fds.fd = fd;
	fds.events = POLLIN;
		
	while (1) {
		ret = poll(&fds, 1, 500);
		if (ret) {	/* 数据有效 */
			ret = read(fd, &data, sizeof(data));
			if(ret < 0) {
				/* 读取错误 */
			} else {
				if(data)
					printf("key value = %d \r\n", data);
			} 	
		} else if (ret == 0) { 	/* 超时 */
			/* 用户自定义超时处理 */
		} else if (ret < 0) {	/* 错误 */
			/* 用户自定义错误处理 */
		}
	}
#endif

	while (1) {	
		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);
		/* 构造超时时间 */
		timeout.tv_sec = 0;
		timeout.tv_usec = 500000; /* 500ms */
		ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
		switch (ret) {
			case 0: 	/* 超时 */
				/* 用户自定义超时处理 */
				break;
			case -1:	/* 错误 */
				/* 用户自定义错误处理 */
				break;
			default:  /* 可以读取数据 */
				if(FD_ISSET(fd, &readfds)) {
					ret = read(fd, &data, sizeof(data));
					if (ret < 0) {
						/* 读取错误 */
					} else {
						if (data)
							printf("key value=%d\r\n", data);
					}
				}
				break;
		}	
	}

	close(fd);
	return ret;
}

主要来看下while(1)里面这段非阻塞处理的程序。
首先:FD_ZERO 宏用于初始化文件描述符集,将其清空,确保没有任何文件描述符被设置在集合中。这是使用 select 函数前的必要步骤,因为 select 函数会检查传入的文件描述符集来决定需要监听哪些文件描述符的状态。如果不先清空文件描述符集,那么可能会包含一些无效或不想监听的文件描述符,从而导致不可预测的行为。
FD_SET 宏用于向文件描述符集中添加一个特定的文件描述符。这意味着您告诉 select 函数,您对这个特定的文件描述符(fd)感兴趣,希望监听它的读状态(是否有数据可读)。每次调用 FD_SET 都会将一个文件描述符加入到集合中。
在使用 select 函数时,第一个参数需要特别注意,它是 nfds,这个参数指定了要检查的文件描述符集中的最高文件描述符加一。这是因为 select 函数使用这个值来确定需要检查的文件描述符范围,从文件描述符 0 到 nfds-1。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1857523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【初阶数据结构】二叉树(附题)

目录 1.树概念及结构 1.1树的概念 1.2 树的相关概念&#xff08;树结构的相关概念命名参考自然树和人的血缘关系&#xff09; 1.3 树的表示 1.4 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff0c;初次之外网盘中使用到&#xff09; 2.二叉树概念及结构 …

Google浏览器快捷方式固定到任务栏启动被其他网页劫持

场景复现 1、Google浏览器设置启动时继续浏览上次打开的网页 2、先浏览CSDN网站&#xff0c;然后关闭Google浏览器 3、再次打开Google浏览器时&#xff0c;除了显示我们上次浏览的CSDN网页外&#xff0c;还默认打开了百度网页 解决办法 1、在Google浏览器中新建标签页&am…

Redis缓存雪崩(主从复制、哨兵模式(脑裂)、分片集群)

缓存雪崩&#xff1a; 在同一时段大量的缓存key同时失效或者Redis服务宕机&#xff0c;导致大量请求到达数据库&#xff0c;带来巨大压力。 方法一&#xff1a; 给不同key的TTL添加随机值&#xff0c;以此避免同一时间大量key失效。&#xff08;用于解决同一时间大量key过期&…

linux笔记10--编辑器之神VIM

文章目录 1. 简单介绍① 为什么叫vim② linux常见的编辑器③ 注意事项④ 其它 2. 操作模式的划分① 两种 -- 国际上普通模式(命令操作模式)插入模式 ② 三种 -- 国内普通模式如何进入与退出界面 插入模式如何进入与退出界面 命令模式如何进入与退出界面常见的命令模式 ③ 区别④…

RFID技术在人工晶体清洗台上的应用案例分析

RFID技术在人工晶体清洗台上的应用案例分析 应用背景 在医疗领域中人工晶体清洗台发挥着极为重要的作用&#xff0c;随着市场需求的持续增长、技术的不断创新、定制化趋势的加强以及环保要求的提高&#xff0c;人工晶体清洗台不免暴露出一下应用痛点需要解决。 痛点&#xff…

SAP ABAP 常用的便利小手段:大写+自动对齐

目录 一&#xff0c;字体变大写 二&#xff0c;自动对齐行 一&#xff0c;字体变大写 找到上面的ユーティリティ⇒設定、 在【ユーザ固有の設定】里选择&#xff0c;【打文字】&#xff0c;同时勾除【名称を変更しない】 二&#xff0c;自动对齐行 在页面右下角找到黄色的【…

Java如何快速实现发送模版消息?

Java如何快速实现发送模版消息&#xff1f; 这次分析模版消息&#xff1a; 公众号&#xff08;小程序同理&#xff09;登录微信公众平台&#xff0c;创建模版&#xff0c;拿到模版id, 拿到appid,appSecret&#xff0c;根据开发文档找到对应功能的api进行开发即可&#xff0c;记…

EcmaScript6全新语法特性-----EcmaScript6(1)

age : 20,language : "Eng"}// 对象也可以用结构表达式来获取对应的值const { name,age,language} person;// 这样可以将我们获取的值name变成abc这个变量// const { name:abc,age,language} person;// 字符串拓展let str "Hello,vue";// 判断是否以xxx…

ES6 逐点突破系列 -- 函数的扩展

} f() // 1 var x 1; function foo(x, y function() { x 2; }) { var x 3; y(); console.log(x); } foo() // 3 x // 1 上面代码中&#xff0c;函数foo的参数形成一个单独作用域。这个作用域里面&#xff0c;首先声明了变量x&#xff0c;然后声明了变量y&#xf…

Axios发送ajax请求

}, // 请求体参数 data: { username: ‘admin’, password: ‘admin’ } }).then(response>{ // 响应状态码 console.log(response.status); // 响应状态字符串 console.log(response.statusText); // 响应头信息 console.log(response.headers); // 响应体 c…

docker 部署的 wordpress 接入阿里云短信服务 详细实操介绍

一、阿里云短信服务配置&#xff1a; 1、登录 阿里云短信服务 完成指引短信相关配置 2、创建RAM用户 并完成授权 出于安全及规范考虑 需通过RAM 用户来完成OponApl 接口调用&#xff0c;创建成功需完成短信接口&#xff08;AliyunDysmsFullAccess、AliyunDysmsReadOnlyAccess…

量检具管理有一套

量检具是用于测量和检验产品尺寸、形状和质量的工具。有一位年轻的工程师小张&#xff0c;他负责管理工厂的量检具&#xff0c;确保它们能够准确地测量产品尺寸和质量。有一天&#xff0c;小张发现量检具出现了一些问题。他注意到一些量具的读数不准确&#xff0c;导致生产出来…

加载资源文件失败

背景 自己以前装了一个海康的深度学习算法平台&#xff0c;试用期是一个月&#xff0c;过了一个月之后&#xff0c;因为没有有效注册码或者加密狗的支持了导致无法使用&#xff0c;于是打算卸载掉&#xff0c;在卸载一个软件的时候&#xff0c;无论是使用控制面板还是软件自带的…

SpringIOC核心源码

一、Spring IOC容器源码解析 1、Spring IOC容器的核心类 &#xff08;1&#xff09;BeanFactory与ApplicationContext &#xff08;2&#xff09;默认容器DefaultListableBeanFactory a. DefaultListableBeanFactory实现的接口 b.DefaultListableBeanFactory继承的类&#…

TCP/IP 在 Linux 内核中的实现

之前出了一个python的socket编程的文章&#xff0c;里面讲的是怎么进行socket编程。最近想到TCP/IP协议的原理&#xff0c;然后查阅资料后说是在操作系统级别实现的&#xff0c;python的socket模块只是一个接口。 本文就来谈一下Linux源码里实现TCP/IP协议簇的源代码在哪里&am…

记一次服务器崩溃事件

今天在安装Jenkins的时候&#xff0c;进行到插件安装这一步&#xff0c;本来一切顺利&#xff0c;结果最后安装完成之后一直进不去网页&#xff0c;显示连接超时&#xff0c;网上搜索了一圈也没发现什么相似的情况&#xff0c;当我疑惑的时候回到Linux控制台&#xff0c;发现命…

三品PDM电子行业解决方案介绍 电子企业PDM应用效果

随着全球化和技术创新的不断推进&#xff0c;电子行业正经历着前所未有的发展机遇。然而&#xff0c;随之而来的挑战也日益凸显&#xff0c;尤其是在产品数据管理PDM方面。本文将探讨电子行业在PDM方面的需求&#xff0c;并提出相应的解决方案&#xff0c;以帮助企业提升效率和…

css布局中的BFC问题

什么是边距重叠&#xff1f; 什么情况下会发生边距重叠&#xff1f; 如何解决边距重叠&#xff1f; 边距重叠&#xff1a; 两个box如果都设置了边距&#xff0c;那么在垂直方向上&#xff0c;两个box的边距会发生重叠&#xff0c;以绝对值大的那个为最终结果显示在页面上。…

解锁数据潜力:数据提取技术的无限可能

在当今信息爆炸的时代&#xff0c;数据已经渗透到我们生活的方方面面&#xff0c;成为推动社会进步和经济发展的重要力量。然而&#xff0c;仅仅拥有数据并不足以带来实质性的改变&#xff0c;关键在于如何有效地提取、分析和利用这些数据&#xff0c;以解锁其深藏的潜力。数据…

计算机中丢失vcruntime140_1.dll如何修复?分享7种常用解决方法

vcruntime140_1.dll缺失原因分析 软件卸载或更新不当在软件卸载或更新过程中&#xff0c;如果未能正确处理依赖关系&#xff0c;可能会导致vcruntime140_1.dll文件被错误地删除或损坏。 据统计&#xff0c;约有30%的DLL文件丢失问题是由于不当的卸载或更新操作引起的。 用户…