18 Linux 阻塞和非阻塞 IO

一、阻塞和非阻塞 IO

1. 阻塞和非阻塞简介

  这里的 IO 指 Input/Output(输入/输出),是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将对应应用程序对应的线程挂起,直到设备资源可以获取为止。非阻塞式 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,知道设备资源可用,要么就直接放弃。

  下图是阻塞式 IO 访问示意图:

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

  非阻塞 IO 访问示意图如下:

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

  使用应用程序来实现阻塞访问:

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

  使用应用程序来实现非阻塞访问:

int fd;
int data = 0;fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);  /* 非阻塞方式打开 */    // O_NONBLOCK参数就是开启非阻塞的关键
ret = read(fd, &data, sizeof(data));             /* 读取数据 */

  当打开设备的时候,默认是阻塞 IO,所以之前的实验都是阻塞 IO,当在打开设备的时候添加 O_NONBLOCK 参数,即可设置为非阻塞方式。

2. 等待队列(唤醒阻塞工作)

① 等待队列头

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

  Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,就必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head 表示。 如果你的代码要考虑移植到老版本 linux 内核中,那么最好使用 wait_queue_head_t 表示等待队列头。
定义完成后需要使用 init_waitqueue_head 函数初始化等待队列头:

// wq_head 就是要初始化的队列头
void init_waitqueue_head(struct wait_queue_head *wq_head);// 也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义和初始化

② 等待队列项

  等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的队列项添加到等待队列里面(A设备不可用,把A设备对应的项放在等待队列项中)。等待队列项结构体 wait_queue_entry

  使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项:

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

③ 队列项添加/移除等待队列头

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

  等待队列项添加/移除 API 函数:

/** @description : 添加等待队列项到等待队列头里* @param - wq_head : 需要加入的等待队列头* @param - wq_entry : 需要加入的等待队列项* @return : 没有返回值*/
void add_wait_queue(struct wait_queue_head *wq_head,struct wait_queue_entry *wq_entry)
/** @description : 移除等待队列头中的等待队列项* @param - wq_head : 等待队列头* @param - wq_entry : 等待队列项* @return : 没有返回值*/
void remove_wait_queue(struct wait_queue_head *wq_head,struct wait_queue_entry *wq_entry)

④ 等待唤醒

  当设备可以使用的时候就要唤醒进程:

void wake_up(struct wait_queue_head *wq_head);
void wake_up_interruptible(struct wait_queue_head *wq_head);/* wq_head:需要唤醒的等待队列头,会将这个队列头的所有进程唤醒区别:wake_up能唤醒TASK_INTERRUPTIBLE TASK_INTERRUPTIBLE,wake_up_interruptible只能唤醒TASK_INTERRUPTIBLE状态的进程*/

⑤ 等待事件

  除了主动唤醒外,也可以设置等待队列去等待某个事件,当这个事件满足后就自动唤醒等待队列中的进程。就相当于在睡眠态中,如果满足某个事件,就自动唤醒队列中的进程。

函数描述
wait_event(wq_head, condition)等待以 wq_head 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞。此函数会将进程设置为 TASK_UNINTERRUPTIBLE 状态
wait_event_timeout(wq_head, condition, timeout)功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition 为假。为 1 的话表示condition 为真,也就是条件满足了。
wait_event_interruptible(wq_head, condition)与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。
wait_event_interruptible_timeout(wq_head,
condition, timeout)
与 wait_event_timeout 函数类似,此函数也将进程设置为TASK_INTERRUPTIBLE,可以被信号打断。

3. 轮询(用户空间)

  如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供轮询。应用程序中式是通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话从设备读取或向设备写入数据。但在驱动程序中,只有 poll 函数。

① select 函数

/*nfds :所要监视的这三类文件描述集合中,最大文件描述符加1readfds、writefds和exceptfds:这三个指针指向描述符集合。readfds监视这些文件是否可读,只要这些文件里有一个可以读取,那么select会返回一个大于0的值表示文件可以读取,如果没有文件可以读取,那就会根据timeout参数判断是否超时。可以将readfds设置为NULL,表示不关心任何文件的读变化,writefds和readfds类似exceptfds监视文件的异常timeout :超时时间,调用 select 函数等待某些文件描述符可以设置超时时间返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数*/
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

  超时时间使用结构体 timeval 表示。这里的超时需要定义一个结构体,并设置好成员参数,再把结构体放在 timeout 上。

  比如现在需要从设备文件中读取数据,那就可以定义一个 fd_set 变量,将这个变量传递给参数 readfds。定义完一个 fd_set 变量后可以使用以下宏进行操作:

void FD_ZERO(fd_set *set);            // 用户将fd_set变量所有位清零
void FD_SET(int fd, fd_set *set);     // 用户将fd_set变量的某个位置1
void FD_CLR(int fd, fd_set *set);     // 用户将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除
int FD_ISSET(int fd, fd_set *set);    // 测试一个文件是否属于某个集合,fd就是要判断的文件描述符

  使用 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);    // 第一个参数 nfds 表示要监视的文件描述符集合中最大的文件描述符值加 1switch (ret) {case 0: /* 超时 */printf("timeout!\r\n");break;case -1: /* 错误 */printf("error!\r\n");break;default: /* 可以读取数据 */if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 *//* 使用 read 函数读取数据 */}break;}
}

② poll 函数

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

/** @param - fds : 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素是结构体pollfd类型* @param - nfds : poll 函数要监视的文件描述符数量* @param - timeout : 超时时间,单位为 ms* @return : 返回发生事件或错误的文件描述符数量;0,超时; -1,发生错误,并且设置 errno 为错误类型*/
int poll(struct pollfd *fds,nfds_t nfds,int timeout);

  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*/

  使用 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 > 0) { /* 数据有效 */// 读取数据} else if (ret == 0) { /* 超时 */// 处理超时情况} else if (ret < 0) { /* 错误 */// 处理错误情况}
}

③ epoll 函数

  之前的 select 和 poll 函数都会随着监听的 fd 数量的增加,出现效率低下的问题,而且 poll 函数每次必须遍历所有的描述符来检查就绪的描述符。epoll 是为了处理高并发(系统需要同时处理大量的并发连接或请求),一般再网络编程中用到 epoll。

  应用程序中需要使用 epoll_create 函数创建 epoll 句柄

/* size :填大于0的值即可返回值 : epoll句柄,如果是-1则创建失败*/
int epoll_create(int size);

   epoll 句柄创建完成后使用 epoll_ctl 函数添加需要监视的文件描述符和事件:

/*epfd :需要操作的epoll句柄op :对epoll句柄进行的操作fd :要监视的文件描述符event :要监视的事件类型返回值 :0,成功; -1,失败,并且设置 errno 的值为相应的错误码。*/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
/*op进行的操作可以设置如下:EPOLL_CTL_ADD 向 epfd(epoll句柄) 添加文件参数 fd 表示的描述符。EPOLL_CTL_MOD 修改参数 fd 的 event 事件。EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。*/

   这里的 eventepoll_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 里面。*/

  上面的事件都可以进行 或 操作,也就是说一次可以设置监视多个事件。

  以上设置好以后应用程序可以通过 epoll_wati 函数来等待事件的发生:

/* epfd :要等待的 epollevents :指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件maxevents :events 数组大小,必须大于 0timeout :超时时间,单位为 ms返回值 :0,超时; -1,错误;其他值,准备就绪的文件描述符数量*/
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

  使用 epoll 函数对驱动文件进行读非阻塞访问:

void main(void) {int ret;int fd; /* 要监视的文件描述符 */int epoll = epoll_create(1); // 创建epoll句柄fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 *//* 构造结构体 */struct epoll_event event;   // 创建event结构体变量event.events = EPOLLIN;     // 这里可以 或上其他想监视的事件ret = epoll_ctl(epoll, EPOLL_CTL_ADD, fd, &event);     if (ret == 0) { /* 成功 */// 处理成功情况} else if (ret < 0) { /* 失败 */    // 失败会自动设置 errno 相应的错误码// 处理失败情况}ret = epoll_wait(epoll, &event, 4, 500);    // 等待事件发生if (ret == 0) { /* 超时 */{// 处理超时情况}else if (ret < 0)   /* 错误 */{// 出现错误情况}else    /* 准备就绪的文件描述符数量*/{// 读取数据}}

4. poll 操作函数(内核空间)

  当应用程序调用 select 或 poll 函数对驱动程序进行非阻塞访问的时候,驱动程序 file_operations 中的 poll 函数就会执行。

/*filp :要打开的设备文件wait :由应用程序传递进来,一般将此参数传递给poll_wait函数返回值 :向应用程序返回设备或者资源状态*/
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);/*返回的资源状态:POLLIN 有数据可以读取。POLLPRI 有紧急的数据需要读取。POLLOUT 可以写数据。POLLERR 指定的文件描述符发生错误。POLLHUP 指定的文件描述符挂起。POLLNVAL 无效的请求。POLLRDNORM 等同于 POLLIN,普通数据可读*/

  我们需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中:

/*description :将一个等待队列头添加到轮询表中,以便在poll函数中对其进行轮询,并在事件发生时唤醒等待的进程filp :设备文件wait_address :添加到 poll_table 中的等待队列头p :poll_table,就是 file_operations 中 poll 函数的 wait 参数*/
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

二、阻塞 IO 实验

  首先拿出上一节的中断按键来加载驱动,并输入以下命令:

./keyirqApp /dev/key &

  测试正常后输入 top 命令查看 keyirqApp 应用程序的 CPU 使用率:

  可以看出就这一个应用程序就占了 CPU 的 50%,原因是我们一直在用 while 循环通过 read 函数读取按键值,因此进程会一直运行。最好的办法就是在没有按键事件的时候,keyirqApp 应用处于休眠状态,当有按键发生 keyirqApp 应用程序才运行,这样可以降低 CPU 使用率。

2.1 程序编写

  这次是在 13_irq 基础上完成。

cd linux/atk-mpl/Drivers/
mkdir 14_blockio

   将 13_irq 文件夹里的 keyirq.c、keyirqApp.c 和 Makefile 复制到 14_blockio,并重命名为 blockio。修改 blockio.c:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*//* 定义按键状态 */
enum key_status {KEY_PRESS = 0,      /* 按键按下 */ KEY_RELEASE,        /* 按键松开 */ KEY_KEEP,           /* 按键状态保持 */ 
};/* key设备结构体 */
struct key_dev{dev_t devid;				/* 设备号 	 */struct cdev cdev;			/* cdev 	*/struct class *class;		/* 类 		*/struct device *device;		/* 设备 	 */struct device_node	*nd; 	/* 设备节点 */int key_gpio;				/* key所使用的GPIO编号		*/struct timer_list timer;	/* 按键值 		*/int irq_num;				/* 中断号 		*/atomic_t status;            /* 按键状态 */		// 这里使用了原子操作wait_queue_head_t r_wait;	/* 读等待队列头 */	// wait_queue_head_t是为了兼容老版本
};static struct key_dev key;          /* 按键设备 *//* 中断进入定时器,定时时间是把按键抖动给延时掉 */
static irqreturn_t key_interrupt(int irq, void *dev_id)	// 中断处理函数
{/* 按键防抖处理,开启定时器延时15ms */mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));	// 为什么需要周期性的定时器,是因为每当检测到按下一次就需要定时器延时return IRQ_HANDLED;		// IRQ_HANDLED是一个预定义的常量,表示中断已经得到处理,并且处理程序成功执行了必要的操作
}/** @description	: 初始化按键IO,open函数打开驱动的时候* 				  初始化按键所使用的GPIO引脚。* @param 		: 无* @return 		: 无*/
static int key_parse_dt(void)
{int ret;const char *str;/* 设置LED所使用的GPIO *//* 1、获取设备节点:key */key.nd = of_find_node_by_path("/key");if(key.nd == NULL) {printk("key node not find!\r\n");return -EINVAL;}/* 2.读取status属性 */ret = of_property_read_string(key.nd, "status", &str);if(ret < 0) return -EINVAL;if (strcmp(str, "okay"))return -EINVAL;/* 3、获取compatible属性值并进行匹配 */ret = of_property_read_string(key.nd, "compatible", &str);if(ret < 0) {printk("key: Failed to get compatible property\n");return -EINVAL;}if (strcmp(str, "alientek,key")) {printk("key: Compatible match failed\n");return -EINVAL;}/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);if(key.key_gpio < 0) {printk("can't get key-gpio");return -EINVAL;}/* 5 、获取GPIO对应的中断号 */key.irq_num = irq_of_parse_and_map(key.nd, 0);if(!key.irq_num){return -EINVAL;}printk("key-gpio num = %d\r\n", key.key_gpio);return 0;
}/* 主要进行GPIO和中断的初始化 */
static int key_gpio_init(void)
{int ret;unsigned long irq_flags;/* 使用GPIO就要申请GPIO使用权 */ret = gpio_request(key.key_gpio, "KEY0");if (ret) {printk(KERN_ERR "key: Failed to request key-gpio\n");return ret;}	/* 将GPIO设置为输入模式 */gpio_direction_input(key.key_gpio);/* 获取设备树中指定的中断触发类型 */irq_flags = irq_get_trigger_type(key.irq_num);		// 获得定义的中断触发类型if (IRQF_TRIGGER_NONE == irq_flags)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;/* 申请中断(使用中断必须申请中断) */ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);	// request_irq会默认使能中断,所以不需要enable_irq使能中断if (ret) {gpio_free(key.key_gpio);return ret;}// 建议申请成功后先用disbale_irq函数禁止中断,等所有工作完成之后再来使能中断return 0;
}/* 定时器处理函数 */
static void key_timer_function(struct timer_list *arg)
{static int last_val = 1;	// 保存按键上一次读取到的值int current_val;		// 存放当前按键读取的值/* 读取按键值并判断按键当前状态 */current_val = gpio_get_value(key.key_gpio);if (0 == current_val && last_val)        	// 读取的值为0,上次的值为1,则是按下{atomic_set(&key.status, KEY_PRESS);	/* 按下 */wake_up_interruptible(&key.r_wait);		// 唤醒}else if (1 == current_val && !last_val){atomic_set(&key.status, KEY_RELEASE);  	/* 松开 */wake_up_interruptible(&key.r_wait);		// 唤醒}elseatomic_set(&key.status, KEY_KEEP);      /* 状态保持 */ last_val = current_val;
}/** @description		: 打开设备* @param - inode 	: 传递给驱动的inode* @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量* 					  一般在open的时候将private_data指向设备结构体。* @return 			: 0 成功;其他 失败*/
static int key_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description     : 从设备读取数据 * @param – filp        : 要打开的设备文件(文件描述符)* @param – buf     : 返回给用户空间的数据缓冲区* @param – cnt     : 要读取的数据长度* @param – offt        : 相对于文件首地址的偏移* @return          : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{int ret;/* 函数会导致当前进程进入睡眠状态,直到有其他进程通过调用wake_up_interruptible来唤醒等待队列上的进程 */ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&key.status));	// 等待按键状态变化的条件成立,从而保证进程只会在按键状态变化时被唤醒if (ret){return ret;}/* 将按键状态信息发送给应用程序 */ret = copy_to_user(buf, &key.status, sizeof(int));/* 状态重置 */atomic_set(&key.status, KEY_KEEP);return ret;
}/** @description		: 向设备写数据 * @param - filp 	: 设备文件,表示打开的文件描述符* @param - buf 	: 要写给设备写入的数据* @param - cnt 	: 要写入的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 写入的字节数,如果为负值,表示写入失败*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description		: 关闭/释放设备* @param - filp 	: 要关闭的设备文件(文件描述符)* @return 			: 0 成功;其他 失败*/
static int key_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.read = key_read,.write = key_write,.release = 	key_release,
};/** @description	: 驱动入口函数* @param 		: 无* @return 		: 无*/
static int __init mykey_init(void)
{int ret;/* 初始化等待队列头 */init_waitqueue_head(&key.r_wait);/* 初始化按键状态(初始化原子变量)*/atomic_set(&key.status, KEY_KEEP);/* 设备树解析 */ret = key_parse_dt();if(ret)return ret;/* GPIO 中断初始化 */ret = key_gpio_init();if(ret)return ret;/* 注册字符设备驱动 *//* 1、创建设备号 */ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);goto free_gpio;}/* 2、初始化cdev */key.cdev.owner = THIS_MODULE;cdev_init(&key.cdev, &key_fops);/* 3、添加一个cdev */ret = cdev_add(&key.cdev, key.devid, KEY_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */key.class = class_create(THIS_MODULE, KEY_NAME);if (IS_ERR(key.class)) {goto del_cdev;}/* 5、创建设备 */key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);if (IS_ERR(key.device)) {goto destroy_class;}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */timer_setup(&key.timer, key_timer_function, 0);return 0;destroy_class:class_destroy(key.class);
del_cdev:cdev_del(&key.cdev);
del_unregister:unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:free_irq(key.irq_num, NULL);gpio_free(key.key_gpio);return -EIO;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit mykey_exit(void)
{/* 注销字符设备驱动 */cdev_del(&key.cdev);/*  删除cdev */unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */del_timer_sync(&key.timer);		/* 删除timer */device_destroy(key.class, key.devid);/*注销设备 */class_destroy(key.class); 		/* 注销类 */free_irq(key.irq_num, NULL);	/* 释放中断 */gpio_free(key.key_gpio);		/* 释放IO */
}module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

  因为 Linux 驱动处理阻塞 IO 需要用到等待队列,而等待队列会使进程进入睡眠, 当能引起睡眠的时候,一定不能使用自旋锁。

  简化版阻塞 IO 流程:

  在等待队列实现阻塞访问注意两点:

  ① 将任务或进程加入到等待队列头里(代码中是通过 wait_event_interruptible加入到等待队列中);

  ② 在合适的地方唤醒等待队列,一般是在中断处理函数里。

2.2 运行测试

  修改 Makefile 文件,将 obj-m := keyirq.o 修改为 obj-m := blockio.o

  编译 blockio.c 和 blockioApp.c 文件:

make
arm-none-linux-gnueabihf-gcc blockioApp.c -o blockioApp

  将 blockioApp 和 blockio.ko 文件复制:

sudo cp blockioApp blockio.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  开启开发板,输入命令:

cd lib/modules/5.4.31/
depmod
modprobe blockio.ko

   测试 App:

./blockioApp /dev/key &

  输入 top 查看 blockioApp 应用的 CPU 使用率:

  当我们在按键驱动程序里加入阻塞访问后,blockioApp 应用使用 CPU 使用率直线下降。这里的 0% 并不是 0,而是 0.0001%,使用率太低会导致这种情况。

  因为之前用了 &,这个指在后台运行,那么如何关闭呢?使用 kill 命令即可。首先使用 ps 查看当前的进程:

  知道了 blockioApp 进程 PID 为 197,使用 kill 命令:

kill -9 197    # -9发送强制终止信号

  之后就没有 blockioApp。

  卸载驱动:

rmmod blockio.ko

三、非阻塞 IO 实验

3.1 驱动程序编写

  创建 15_noblockio,并创建 VScode 工作区,并把 blockio.c 文件复制到 15_noblockio 文件夹内,并重命名为 noblockio.c。修改 noblock.c 文件:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>		// 新增 poll.h 因为有使用到 poll 函数
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*//* 定义按键状态 */
enum key_status {KEY_PRESS = 0,      // 按键按下KEY_RELEASE,        // 按键松开KEY_KEEP,           // 按键状态保持
};/* key设备结构体 */
struct key_dev{dev_t devid;			/* 设备号 	 */struct cdev cdev;		/* cdev 	*/struct class *class;	/* 类 		*/struct device *device;	/* 设备 	 */struct device_node	*nd; /* 设备节点 */int key_gpio;			/* key所使用的GPIO编号		*/struct timer_list timer;			/* 按键值 		*/int irq_num;			/* 中断号 		*/atomic_t status;   		/* 按键状态 */wait_queue_head_t r_wait;	/* 读等待队列头 */
};static struct key_dev key;          /* 按键设备 */static irqreturn_t key_interrupt(int irq, void *dev_id)
{/* 按键防抖处理,开启定时器延时15ms */mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));return IRQ_HANDLED;
}/** @description	: 初始化按键IO,open函数打开驱动的时候* 				  初始化按键所使用的GPIO引脚。* @param 		: 无* @return 		: 无*/
static int key_parse_dt(void)
{int ret;const char *str;/* 设置LED所使用的GPIO *//* 1、获取设备节点:key */key.nd = of_find_node_by_path("/key");if(key.nd == NULL) {printk("key node not find!\r\n");return -EINVAL;}/* 2.读取status属性 */ret = of_property_read_string(key.nd, "status", &str);if(ret < 0) return -EINVAL;if (strcmp(str, "okay"))return -EINVAL;/* 3、获取compatible属性值并进行匹配 */ret = of_property_read_string(key.nd, "compatible", &str);if(ret < 0) {printk("key: Failed to get compatible property\n");return -EINVAL;}if (strcmp(str, "alientek,key")) {printk("key: Compatible match failed\n");return -EINVAL;}/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);if(key.key_gpio < 0) {printk("can't get key-gpio");return -EINVAL;}/* 5 、获取GPIO对应的中断号 */key.irq_num = irq_of_parse_and_map(key.nd, 0);if(!key.irq_num){return -EINVAL;}printk("key-gpio num = %d\r\n", key.key_gpio);return 0;
}static int key_gpio_init(void)
{int ret;unsigned long irq_flags;ret = gpio_request(key.key_gpio, "KEY0");if (ret) {printk(KERN_ERR "key: Failed to request key-gpio\n");return ret;}	/* 将GPIO设置为输入模式 */gpio_direction_input(key.key_gpio);/* 获取设备树中指定的中断触发类型 */irq_flags = irq_get_trigger_type(key.irq_num);if (IRQF_TRIGGER_NONE == irq_flags)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;/* 申请中断 */ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);if (ret) {gpio_free(key.key_gpio);return ret;}return 0;
}static void key_timer_function(struct timer_list *arg)
{static int last_val = 1;int current_val;/* 读取按键值并判断按键当前状态 */current_val = gpio_get_value(key.key_gpio);if (0 == current_val && last_val){atomic_set(&key.status, KEY_PRESS);	// 按下wake_up_interruptible(&key.r_wait);	// 唤醒r_wait队列头中的所有队列}else if (1 == current_val && !last_val) {atomic_set(&key.status, KEY_RELEASE);   // 松开wake_up_interruptible(&key.r_wait);	// 唤醒r_wait队列头中的所有队列}elseatomic_set(&key.status, KEY_KEEP);              // 状态保持last_val = current_val;
}/** @description		: 打开设备* @param - inode 	: 传递给驱动的inode* @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量* 					  一般在open的时候将private_data指向设备结构体。* @return 			: 0 成功;其他 失败*/
static int key_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description     : 从设备读取数据 * @param – filp        : 要打开的设备文件(文件描述符)* @param – buf     : 返回给用户空间的数据缓冲区* @param – cnt     : 要读取的数据长度* @param – offt        : 相对于文件首地址的偏移* @return          : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{int ret;// 判断是否是非阻塞读取访问if (filp->f_flags & O_NONBLOCK) {	// 非阻塞方式访问if(KEY_KEEP == atomic_read(&key.status))	// 判断是否按下或松开按键,如果没有就返回-EAGAINreturn -EAGAIN;} else {							// 阻塞方式访问/* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&key.status));if(ret)return ret;}/* 将按键状态信息发送给应用程序 */ret = copy_to_user(buf, &key.status, sizeof(int));/* 状态重置 */atomic_set(&key.status, KEY_KEEP);return ret;
}/** @description		: 向设备写数据 * @param - filp 	: 设备文件,表示打开的文件描述符* @param - buf 	: 要写给设备写入的数据* @param - cnt 	: 要写入的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 写入的字节数,如果为负值,表示写入失败*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description		: 关闭/释放设备* @param - filp 	: 要关闭的设备文件(文件描述符)* @return 			: 0 成功;其他 失败*/
static int key_release(struct inode *inode, struct file *filp)
{return 0;
}/** @description     : poll函数,用于处理非阻塞访问,当用户空间调用 select/poll时候,key_poll函数就会执行* @param - filp    : 要打开的设备文件(文件描述符)* @param - wait    : 等待列表(poll_table)* @return          : 设备或者资源状态,*/
static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{unsigned int mask = 0;/* 调用 poll_wait 函数将等待队列头添加到 poll_table 中 */poll_wait(filp, &key.r_wait, wait);		// file_operations 中 poll 函数的 wait 参数if(KEY_KEEP != atomic_read(&key.status))	// 按键按下或松开动作发生mask = POLLIN | POLLRDNORM;	// 返回PLLINreturn mask;
}/* 设备操作函数 */
static struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.read = key_read,.write = key_write,.release = 	key_release,.poll = key_poll,		// 
};/** @description	: 驱动入口函数* @param 		: 无* @return 		: 无*/
static int __init mykey_init(void)
{int ret;/* 初始化等待队列头 */init_waitqueue_head(&key.r_wait);/* 初始化按键状态 */atomic_set(&key.status, KEY_KEEP);/* 设备树解析 */ret = key_parse_dt();if(ret)return ret;/* GPIO 中断初始化 */ret = key_gpio_init();if(ret)return ret;/* 注册字符设备驱动 *//* 1、创建设备号 */ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);goto free_gpio;}/* 2、初始化cdev */key.cdev.owner = THIS_MODULE;cdev_init(&key.cdev, &key_fops);/* 3、添加一个cdev */ret = cdev_add(&key.cdev, key.devid, KEY_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */key.class = class_create(THIS_MODULE, KEY_NAME);if (IS_ERR(key.class)) {goto del_cdev;}/* 5、创建设备 */key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);if (IS_ERR(key.device)) {goto destroy_class;}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */timer_setup(&key.timer, key_timer_function, 0);return 0;destroy_class:device_destroy(key.class, key.devid);
del_cdev:cdev_del(&key.cdev);
del_unregister:unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:free_irq(key.irq_num, NULL);gpio_free(key.key_gpio);return -EIO;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit mykey_exit(void)
{/* 注销字符设备驱动 */cdev_del(&key.cdev);/*  删除cdev */unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */del_timer_sync(&key.timer);		/* 删除timer */device_destroy(key.class, key.devid);/*注销设备 */class_destroy(key.class); 		/* 注销类 */free_irq(key.irq_num, NULL);	/* 释放中断 */gpio_free(key.key_gpio);		/* 释放IO */
}module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

  编写 noblockApp.c:

#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>/** @description     : main主程序* @param – argc        : argv数组元素个数* @param – argv        : 具体参数* @return          : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{fd_set readfds;     /* 读操作文件描述符集 */int key_val;int fd;int ret;/* 判断传参个数是否正确 */if(2 != argc) {printf("Usage:\n""\t./keyApp /dev/key\n");return -1;}/* 打开设备 */fd = open(argv[1], O_RDONLY | O_NONBLOCK);  // O_NONBLOCK非阻塞模式if(0 > fd) {printf("ERROR: %s file open failed!\n", argv[1]);return -1;}FD_ZERO(&readfds);      // 清零FD_SET(fd, &readfds);   // 置一/* 循环轮训读取按键数据 */while (1) {ret = select(fd + 1, &readfds, NULL, NULL, NULL);switch (ret) {case 0:     // 超时/* 用户自定义超时处理 */{printf("Timeout!\n");}break;case -1:        // 错误/* 用户自定义错误处理 */{printf("Something is wrong\n");}break;default:if(FD_ISSET(fd, &readfds)) {read(fd, &key_val, sizeof(int));if (0 == key_val)printf("Key Press\n");else if (1 == key_val)printf("Key Release\n");}break;}}/* 关闭设备 */close(fd);return 0;
}

3.2 运行测试

  编写 Makefile:

KERNELDIR := /home/alientek/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)obj-m := noblockio.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  编译 noblock.c 和 noblockApp.c:

make
arm-none-linux-gnueabihf-gcc noblockioApp.c -o noblockioApp

  将 noblockApp 和 noblock.ko 复制: 

sudo cp noblockioApp noblockio.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  开启开发板,输入以下命令:

cd lib/modules/5.4.31/
depmod
modprobe noblockio.ko

  测试驱动:

./noblockioApp /dev/key &

  之后输入 top 就可以看到:

  这样就完成了非阻塞 IO 实验。

  如果直接卸载驱动的话会出现:

   这个是因为没有 kill 指定后台:kill -9 228

  之后再卸载驱动:rmmod noblockioApp.ko

总结

  概念:

  阻塞和非阻塞,一个是在内核里面,一个是在内核和用户空间反复跳。 他们的概念需要知道。

  阻塞:当一个任务或线程执行某个操作时,如果该操作无法立即完成,任务或线程将会被阻塞(暂停执行)。阻塞式会一直占用 CPU,直到操作完成,这里面有 等待队列头,等待队列项,等待唤醒和等待事件比较重要。

  非阻塞:当一个任务或线程执行某个操作时,如果该操作无法立即完成,任务或线程不会被阻塞,而是立即返回,继续执行后续的任务或代码。非阻塞式调用不会一直占用 CPU 资源,任务或线程可以在等待操作完成时去处理其他任务或代码。这里面有 三个函数 slect、poll、epoll。使用方式得知道。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

作业提交产品的爽点、痛点和痒点分析以及开源软件介绍

目录 1 爽点1.1 什么是爽点1.2 如何构成爽点1.3 案例 2 痛点2.1 什么是痛点2.2 如何构成痛点2.3 案例分析 3 痒点3.1 什么是痒点3.2 如何构成痒点3.3 案例分析 4 开源软件Canvas LMS4.1 链接地址4.2 简介4.3 特点和功能4.4 功能展示图片 1 爽点 1.1 什么是爽点 即时满足&#…

基于SSM的网络音乐系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

将 ONLYOFFICE 文档编辑器与 С# 群件平台集成

在本文中&#xff0c;我们会向您展示 ONLYOFFICE 文档编辑器与其自有的协作平台集成。 ONLYOFFICE 是一款开源办公套件&#xff0c;包括文本文档、电子表格和演示文稿编辑器。这款套件支持用户通过文档编辑组件扩展第三方 web 应用的功能&#xff0c;可直接在应用的界面中使用。…

深度学习(生成式模型)——Classifier Guidance Diffusion

文章目录 前言问题建模条件扩散模型的前向过程条件扩散模型的反向过程条件扩散模型的训练目标 前言 几乎所有的生成式模型&#xff0c;发展到后期都需要引入"控制"的概念&#xff0c;可控制的生成式模型才能更好应用于实际场景。本文将总结《Diffusion Models Beat …

TortoiseSVN 状态图标不显示的两种解决办法

文章目录 TortoiseSVN 方式解决注册表方式解决 TortoiseSVN 方式解决 在桌面或者资源管理器中鼠标右键打开 TortoiseSVN 设置选择 Icon Overlays (图标覆盖)Status cache&#xff08;状态缓存&#xff09; 选择 ‘Shell’ 选择 Icon Overlays&#xff08;图标覆盖&#xff09;…

uniapp和vue3+ts开发小程序,使用vscode提示声明变量冲突解决办法

在uniapp中&#xff0c;我们可能经常会遇到需要在不用的环境中使用不同变量的场景&#xff0c;例如在VUE3中的小程序环境使用下面的方式导入echarts&#xff1a; const echarts require(../../static/echarts.min); 如果不是小程序环境则使用下面的方式导入echarts&#xff…

MobaXterm 安装+使用

目录 1 下载安装 1.1 官网下载(速度慢) 1.2 WebRA下载(不是最新版) 2 常用功能 2.1 基础设置 2.2 常用功能 1 下载安装 1.1 官网下载(速度慢) 点击MobaXterm官网,按下图↓↓步骤下载 1.2 WebRA下载(不是最新版) 点击WebRA网址,按下图↓↓步骤下载 2 常用功能 2.1 基础设…

科普测量开关电源输出波形的三种方法及电源波形自动化测试步骤

开关电源波形测试就是对开关电源的输出波形进行检测和分析&#xff0c;观察开关电源参数变化&#xff0c;以此来判断开关电源的性能是否符合要求。好的开关电源对于设备以及整个电路的正常运行是非常重要的&#xff0c;因此开关电源输出波形测试是开关电源测试的重要环节&#…

python调用chrome实现网页自动操作

一. 内容简介 python调用chrome实现网页自动操作。 二. 软件环境 2.1vsCode 2.2Anaconda version: conda 22.9.0 2.3代码 链接&#xff1a; 三.主要流程 3.1 下载驱动和插件 调用谷歌浏览器&#xff0c;需要下载浏览器驱动&#xff08;https://registry.npmmirror.co…

conda修改虚拟环境名称

conda 修改虚拟环境名称 conda 不能直接更改名称&#xff0c;但是可以通过克隆环境解决 新建环境&#xff08;克隆旧环境&#xff09; conda create --name 新环境名 --clone 旧环境名 删除原环境 conda remove --name 旧环境名 --all 查看现有环境 conda env list conda i…

PMCW体制雷达系列文章(1) – PMCW体制雷达综述

说明 相位调制连续波(Phase-modulated continuous wave, PMCW)雷达&#xff0c;或又被称为数字雷达&#xff0c;近年来开始被应用于汽车雷达领域。而且因其特有的一些优势(精度高、抗干扰能力强等)被认为是车载毫米波雷达的发展趋势之一(从目前占主导的调频连续波(Frequency-mo…

矩阵的除法

B/A 如果矩阵A可逆&#xff0c;那么 证明&#xff1a; A/AB 如果矩阵A和B都可逆&#xff0c;那么 证明&#xff1a;

【教学类-40-05】A4骰子纸模制作5.0(3CM嵌套6条+记录表带符号6条)

作品展示 背景需求 骰子4.0&#xff08;2张长方形纸条&#xff09;存在问题&#xff1a;4.5骰子体积等于幼儿手掌一样大&#xff0c;但是打印纸打印&#xff0c;骰子中间空心&#xff0c;容易捏扁&#xff0c;我想试试是否可以制作更小尺寸的骰子&#xff0c;并用A4卡纸打印&a…

ChatRule:基于知识图推理的大语言模型逻辑规则挖掘11.10

ChatRule&#xff1a;基于知识图推理的大语言模型逻辑规则挖掘 摘要引言相关工作初始化和问题定义方法实验 摘要 逻辑规则对于揭示关系之间的逻辑联系至关重要&#xff0c;这可以提高推理性能并在知识图谱&#xff08;KG&#xff09;上提供可解释的结果。虽然已经有许多努力&a…

常见排序算法之插入排序类

插入排序&#xff0c;是一种简单直观的排序算法&#xff0c;工作原理是将一个记录插入到已经排好序的有序表中&#xff0c;从而形成一个新的、记录数增1的有序表。在实现过程中&#xff0c;它使用双层循环&#xff0c;外层循环对除了第一个元素之外的所有元素&#xff0c;内层循…

百度王颖:百度文库以AI创作能力突破语言边界,促进思想碰撞和文化融通

1月9日&#xff0c;2023年世界互联网大会乌镇峰会“网络传播与文明交流互鉴论坛”召开。百度副总裁、互娱和垂类平台负责人王颖出席并发表“以技术搭建跨文化交流桥梁”主题演讲。她表示&#xff0c;在大模型的加持下&#xff0c;百度各个产品都在重构&#xff0c;通过技术助力…

[C++随笔录] 红黑树

红黑树 红黑树的特点红黑树的模拟实现红黑树的底层结构insert的实现实现思路更新黑红比例的逻辑insert的完整代码 insert的验证 源码 红黑树的特点 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是 Red或 Black。…

FPGA与STM32_FSMC总线通信实验

FPGA与STM32_FSMC总线通信实验 内部存储器IP核的参数设置创建IP核FPGA代码STM32标准库的程序 STM32F407 上自带 FSMC 控制器&#xff0c;通过 FSMC 总线的地址复用模式实现STM32 与 FPGA 之间的通信&#xff0c;FPGA 内部建立 RAM 块&#xff0c;FPGA 桥接 STM32 和 RAM 块&…

机器学习 - 决策树:技术全解与案例实战

目录 一、引言二、决策树基础决策树模型概述构建决策树的关键概念特征选择决策树的生成 决策树的剪枝 三、算法研究进阶提升树和随机森林提升树&#xff08;Boosted Trees&#xff09;随机森林&#xff08;Random Forests&#xff09; 进化算法与决策树决策树结构的进化 多目标…

【图论实战】 Boost学习 03:dijkstra_shortest_paths

文章目录 示例代码 示例 最短路径: A -> C -> D -> F -> E -> G 长度 16 代码 #include <iostream> #include <boost/graph/adjacency_list.hpp> #include <boost/graph/dijkstra_shortest_paths.hpp> #include <boost/graph/graphviz.h…