当前位置: 首页 > news >正文

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。使用方式得知道。

相关文章:

18 Linux 阻塞和非阻塞 IO

一、阻塞和非阻塞 IO 1. 阻塞和非阻塞简介 这里的 IO 指 Input/Output&#xff08;输入/输出&#xff09;&#xff0c;是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候&#xff0c;如果不能获取到设备资源&#xff0c;那么阻塞式 IO 就会将对应应用…...

多因素验证如何让企业邮箱系统登录更安全?

企业邮箱系统作为基础的办公软件之一&#xff0c;既是企业内外沟通的重要工具&#xff0c;也是连接企业多个办公平台的桥梁&#xff0c;往往涉及到客户隐私、业务信息、企业机密等等。为了保护邮箱账户的安全&#xff0c;设置登陆密码无疑是保护账户安全的常用措施之一。然而随…...

投票助手图文音视频礼物打赏流量主小程序开源版开发

投票助手图文音视频礼物打赏流量主小程序开源版开发 图文投票&#xff1a;用户可以发布图文投票&#xff0c;选择相应的选项进行投票。 音视频投票&#xff1a;用户可以发布音视频投票&#xff0c;观看音视频后选择相应的选项进行投票。 礼物打赏&#xff1a;用户可以在投票过…...

黑客(网络安全)技术——高效自学1.0

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c;但是连方向都没搞清楚就开始学习…...

8255 boot介绍及bring up经验分享

这篇文章会简单的介绍8255的启动流程&#xff0c;然后着重介绍8255在实际项目中新硬件上的bring up工作&#xff0c;可以给大家做些参考。 8255 boot介绍 下面这些信息来自文档&#xff1a;《QAM8255P IVI Boot and CoreBSP Architecture Technical Overview》 80-42847-11 R…...

visual studio 启用DPI识别功能

在开发widow程序时&#xff0c;有时必须将电脑 设置-->显示-->缩放与布局-->更改文本、应用项目的大小-->100%后&#xff0c;程序的画面才能正确运行&#xff0c;居说这是锁定了dpi的原因&#xff0c;需要启dpi识别功能。设置方法如下&#xff1a; 或者...

一题三解(暴力、二分查找算法、单指针):鸡蛋掉落

涉及知识点 暴力、二分查找算法、单指针 题目 给你 k 枚相同的鸡蛋&#xff0c;并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。 已知存在楼层 f &#xff0c;满足 0 < f < n &#xff0c;任何从 高于 f 的楼层落下的鸡蛋都会碎&#xff0c;从 f 楼层或比它低的…...

第一章 Object-XML 映射简介

文章目录 第一章 Object-XML 映射简介基础如何工作的映射选项IRIS 中的相关工具XML 文档的可能应用 第一章 Object-XML 映射简介 基础 将对象映射到 XML 一词意味着定义如何将该对象用作 XML 文档。要将对象映射到 XML&#xff0c;请将 %XML.Adaptor 添加到定义该对象的类的超…...

精密设备企业适合哪款CRM客户管理体系?

精密设备企业致力于打造现代化管理体系&#xff0c;以精密的仪器、精细的销售、精准的市场、精确的售后为企业核心&#xff0c;提供优质的精密产品和专业服务。随着企业的发展及市场发展需要&#xff0c;建立高效的客户关系管理体系势在必行。那么&#xff0c;精密设备企业适合…...

Rasa-笔记

1 Rasa环境搭建 笔者使用的Rasa版本是古早的1.10.7&#xff0c;python环境3.7。 1、安装miniconda 2、conda创建python3.7环境 3、安装TensorFlow和GPU相关 4、安装Rasa相关 2 Rasa笔记 3 Rasa报错 3.1 ValueError: Can’t patch loop of type <class ‘uvloop.Loop’&g…...

云架构师学习------腾讯云通识-存储与数据库

云架构师学习------腾讯云通识-存储与数据库 云架构师学习------腾讯云通识-存储与数据库存储基础存储服务对象存储-COS产品概述功能概览产品优势 云硬盘-CBS产品概述产品功能产品优势云硬盘类型 文件存储-CFS产品概述产品功能产品优势文件存储类型及性能规格存储类型性能与规格…...

蓝桥杯之模拟与枚举day1

Question1卡片(C/CA组第一题) 这个是一道简单的模拟枚举题目&#xff0c;只要把对应每次的i的各个位都提取出来&#xff0c;然后对应的卡片数目减去1即可。属于打卡题目。注意for循环的特殊使用即可 #include <iostream> using namespace std; bool solve(int a[],int n…...

深度学习 python opencv 动物识别与检测 计算机竞赛

文章目录 0 前言1 深度学习实现动物识别与检测2 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 3 YOLOV53.1 网络架构图3.2 输入端3.3 基准网络3.4 Neck网络3.5 Head输出层 4 数据集准备4.1 数据标注简介4.2 数据保存…...

爱家房产网站源码 爱家房产网商业版 微信互动营销整合+手机触屏版+经纪人分销

房产网站源码手机访问自动转手机版修改修复如下&#xff1a; 1&#xff0c;修复手机版首页标题头部名称 2&#xff0c;修复手机版首页频道导航按钮 3&#xff0c;新增手机版广告位置显示方式 4&#xff0c;修复手机版首页内容显示样式 5&#xff0c;手机版头部背景颜色ic…...

招聘信息采集

首先&#xff0c;我们需要使用PHP的curl库来发送HTTP请求。以下是一个基本的示例&#xff1a; <?php // 初始化curl $ch curl_init();// 设置代理 curl_setopt($ch, CURLOPT_PROXY, "jshk.com.cn");// 设置URL curl_setopt($ch, CURLOPT_URL, "http://www…...

java开发宝典

Java命名规范 1&#xff1a;代码中的命名均不能以下划线或美元符号开始&#xff0c;也不能以下划线或美元符号结束。 反例&#xff1a;_name / __name / $name / name_ / name$ / name__ 。 2&#xff1a;禁止使用拼音和英文混合。 反例&#xff1a;DaZhePromotion [打折] / …...

【图论实战】 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…...

嵌入式养成计划-52----ARM--开发板介绍--相关硬件基础内容介绍--GPIO讲解

一百三十一、开发板介绍 131.1 核心板介绍 131.2 拓展板 一百三十二、相关硬件基础内容介绍 132.1 PCB PCB&#xff08; Printed Circuit Board&#xff09;&#xff0c;中文名称为印制电路板&#xff0c;又称印刷线路板&#xff0c; 是重要的电子部件&#xff0c;是电子元器…...

线性代数-Python-04:线性系统+高斯消元的实现

文章目录 1 线性系统2 高斯-jordon消元法的实现2.1 Matrix2.2 Vector2.3 线性系统 3 行最简形式4 线性方程组的结构5 线性方程组-通用高斯消元的实现5.1 global5.2 Vector-引入is_zero5.3 LinearSystem5.4 main 1 线性系统 2 高斯-jordon消元法的实现 2.1 Matrix from .Vecto…...

python能用来做什么

Python是一种流行的编程语言&#xff0c;由Guido van Rossum创建&#xff0c;并于1991年发布。 它用于&#xff1a; Web开发&#xff08;服务器端&#xff09;&#xff1b; 软件开发&#xff0c;数学计算&#xff0c;系统脚本编写。 Python能做什么&#xff1f; Python可以…...

UML和面向对象

UML(统一建模语言,Unified Modeling Language)和面向对象(Object-Orientation)是软件工程中紧密相连的两个概念。面向对象是一种程序设计思想,而 UML 是一种可视化建模语言,用于表达面向对象分析(OOA)与设计(OOD)的成果。两者结合,使复杂系统的分析、设计、沟通和文…...

终极Win11系统优化指南:深入解析Win11Debloat架构与技术实现

终极Win11系统优化指南&#xff1a;深入解析Win11Debloat架构与技术实现 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter …...

终极指南:5分钟掌握ComfyUI-BiRefNet-ZHO,轻松实现专业级图像视频抠图

终极指南&#xff1a;5分钟掌握ComfyUI-BiRefNet-ZHO&#xff0c;轻松实现专业级图像视频抠图 【免费下载链接】ComfyUI-BiRefNet-ZHO Better version for BiRefNet in ComfyUI | Both img & video 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-BiRefNet-ZHO …...

ModTheSpire终极指南:5步轻松掌握《杀戮尖塔》模组加载技术

ModTheSpire终极指南&#xff1a;5步轻松掌握《杀戮尖塔》模组加载技术 【免费下载链接】ModTheSpire External mod loader for Slay The Spire 项目地址: https://gitcode.com/gh_mirrors/mo/ModTheSpire 想要为《杀戮尖塔》添加新角色、自定义卡牌或优化游戏体验吗&am…...

从APB到SDA:手把手教你用Verilog搭建一个可配置的I2C Master控制器(附完整RTL代码)

从APB到SDA&#xff1a;手把手教你用Verilog搭建一个可配置的I2C Master控制器&#xff08;附完整RTL代码&#xff09; 在数字IC设计和FPGA开发领域&#xff0c;I2C总线因其简单的两线制结构和灵活的多主从配置&#xff0c;成为连接低速外设的首选方案。本文将带您从零开始&…...

3dsconv:3DS游戏文件转换的终极解决方案,快速将.3ds转为CIA格式

3dsconv&#xff1a;3DS游戏文件转换的终极解决方案&#xff0c;快速将.3ds转为CIA格式 【免费下载链接】3dsconv Python script to convert Nintendo 3DS CCI (".cci", ".3ds") files to the CIA format 项目地址: https://gitcode.com/gh_mirrors/3d/3…...

用Lottie动画和LeanCloud,给你的React Native登录页加点‘魔法’(附完整代码)

用Lottie动画和LeanCloud打造React Native登录页的视觉魔法 在移动应用的世界里&#xff0c;第一印象决定一切。一个枯燥的登录页面可能会让用户对你的应用产生负面印象&#xff0c;而一个精心设计的交互体验则能瞬间提升品牌形象。作为React Native开发者&#xff0c;我们拥有…...

数据脱敏方法

数据脱敏(Data Masking)是一种通过特定规则对敏感数据进行变形、替换或屏蔽的技术,目的是在保留数据可用性的同时,降低数据泄露风险,满足合规要求(如 GDPR、个人信息保护法)。脱敏后的数据可用于开发、测试、分析、培训等非生产环境,或在生产环境对外展示时保护隐私。 …...

Algebird未来展望:抽象代数在大数据领域的创新应用

Algebird未来展望&#xff1a;抽象代数在大数据领域的创新应用 【免费下载链接】algebird Abstract Algebra for Scala 项目地址: https://gitcode.com/gh_mirrors/al/algebird Algebird作为Scala生态中专注于抽象代数的数据处理库&#xff0c;正通过其独特的代数结构为…...

[特殊字符] MoviePy 报错:配置了 ImageMagick 环境变量却不好使?

.This error can be due to the fact that ImageMagick is not installed on your computer, or (for Windows users) that you didnt specify the path to the ImageMagick binary in file conf.py, or that the path you specified is incorrect在使用 Python 的 MoviePy 库制…...