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

Linux驱动开发笔记(十一)——阻塞和非阻塞IO

视频:第14.1讲 Linux阻塞和非阻塞IO实验-阻塞与非阻塞简介_哔哩哔哩_bilibili

资料:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》五十二章


1、概念

1.1 简介

        (直接搬《开发指南》的解释了)

        当应用程序对设备驱动进行操作时,如果不能获取到设备资源,阻塞式IO就会将应用程序对应的进程挂起,直到设备资源可以获取为止。阻塞式IO如图52.1.1.1 所示:

       应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好时,会向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,往复循环,直到数据读取成功。非阻塞IO如图52.1.2所示:

fd = open("/dev/xxx_dev", O_RDWR);              /* 阻塞方式打开 */ 
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */

1.2 阻塞访问——等待队列

1.2.1 等待队列头

        若设备资源变为可用,则需要唤醒休眠的进程。唤醒工作一般由中断函数完成。
        Linux内核提供等待队列wait queue来实现阻塞进程的唤醒。其结构体定义如下:

//定义在include/linux/wait.h
struct __wait_queue_head {spinlock_t lock;struct list_head task_list;
};

        定义好等待队列头,需要对其进行初始化:

void init_waitqueue_head(wait_queue_head_t *q)

         也可以使用宏来一次性完成定义、初始化:

#define DECLARE_WAIT_QUEUE_HEAD(name) \wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

1.2.2 等待队列项

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

//定义在include/linux/wait.h
typedef struct __wait_queue wait_que{unsigned int		flags;void			    *private;wait_queue_func_t	func;struct list_head	task_list;
};

        定义好等待队列等以后需要初始化:

void init_waitqueue_head(wait_queue_head_t *q)
// q就是要初始化的等待队列头

        也可以使用宏定义初始化:

#define DECLARE_WAITQUEUE(name, tsk)					\wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

1.2.3 添加 / 移除队列项

       当设备资源不可访问时,需要将进程对应的等待队列项添加到等待队列头,然后进程才能进入休眠。设备资源可以访问后,再唤醒对应进程并移除对应的等待队列项。

void add_wait_queue(wait_queue_head_t  *q,    // 等待队列头wait_queue_t *wait)       // 要加入的等待队列项
void remove_wait_queue(wait_queue_head_t *q,   // 等待队列头wait_queue_t *wait)     // 要删除的等待队列项

1.2.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状态的进程。

// 定义在include/linux/sched.h
#define TASK_RUNNING		    0    // 运行状态
#define TASK_INTERRUPTIBLE	    1    // 可中断的睡眠状态。一旦被唤醒,会切换为TASK_RUNNING
#define TASK_UNINTERRUPTIBLE    2    // 不可中断的睡眠状态
#define __TASK_STOPPED		    4    // 停止状态。此时进程不再被调度,收到SIGCONT才会恢复到TASK_RUNNING
#define __TASK_TRACED		    8    // 正在被调试器跟踪

1.2.5 等待事件(自动唤醒)

       除了手动唤醒外,还可以将等待队列设置为自动唤醒,当满足某个事件以后自动唤醒等待队列中的进程。

wait_event(wq, condition)
// wq       : 等待队列头
// condition: 唤醒条件条件
// 当condition为真时唤醒wq的等待队列,否则此函数会将进程设置为TASK_UNINTERRUPTIBLE状态
wait_event_timeout(wq, condition, timeout)
// wq       : 等待队列头
// condition: 唤醒条件条件
// timeout  : 超时时间(单位jiffies)
// return   : 返回0表示超时 且condition为假。
//            返回正数表示condition满足时的剩余时间 
wait_event_interruptible(wq, condition)
// 与wait_event类似,但是此函数将进程设置为TASK_INTERRUPTIBLE,可以被信号打断// 比如kill -9 XXX,就是给给进程发送信号9,表示终止进程
// 如果是wait_event_interruptible,就会立刻响应,结束本进程
// 但如果是wait_event,其睡眠不可被中断,不会响应任何信号,这时候用ps查看就会发现无法kill掉
// 只有当进程被唤醒后才会执行kill -9
wait_event_interruptible_timeout(wq, condition, timeout) 
// 与wait_event_timeout类似,此函数也将进程设置为TASK_INTERRUPTIBLE,可以被信号打断

1.3 非阻塞访问——轮询

        如果应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式——轮询。
        poll、epoll和select用于处理轮询。应用程序通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或向设备写入。当应用程序调用select、epoll或poll函数的时候设备驱动程序中的(内核)poll函数就会执行,我们需要在设备驱动程序中编写poll函数。

1.3.1 select

(这里干讲概念还是抽象了,具体用法看1.3.1.5或2.2.4)

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

        要检测的文件描述符的范围,为文件最大描述符+1。

1.3.1.2 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)           // 将fd_set变量的所有位都清零
void FD_SET(int fd, fd_set *set)    // 将fd_set变量的某个位置1,即向fd_set添加一个文件描述符,fd就是要加入的文件描述符
void FD_CLR(int fd, fd_set *set)    // 将fd_set变量的某个位置0,即将一个文件描述符从fd_set中删除,fd就是要删除的文件描述符
int  FD_ISSET(int fd, fd_set *set)  // 用于测试一个文件是否属于set集合,fd就是要判断的文件描述符。
1.3.1.3 timeout

        timeout为超时时间。当我们调用select函数等待某些文件描述符可以设置超时时间,超时时 间使用结构体timeval表示:

// 定义在include/uapi/linux/time.h
struct timeval {__kernel_time_t		    tv_sec;	 /* 秒*/__kernel_suseconds_t	tv_usec; /* 微秒 */
};

timeout为NULL表示无限等待。

1.3.1.4 return

        返回0 :表示超时,且没有任何文件描述符可以进行操作
        返回-1:发生错误;
        返回其他值:可以进行操作的文件描述符个数

1.3.1.5 示例代码

 《开发指南》中给出了示例代码:

// 示例代码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 } 

1.3.2 poll函数

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

int poll(struct pollfd  *fds, // 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型nfds_t   nfds,       // poll函数要监视的文件描述符数量int    timeout)      // 超时时间,单位为ms// return: 
//   返回正数:revents域中不为0的pollfd结构体个数,也就是发生事件或错误的文件描述符数量
//   返回0, 超时
//   返回-1,发生错误

其中struct pollfd的定义为:

struct pollfd { int   fd;      /* 文件描述符  */ short events;    /* 请求的事件  */ short revents;     /* 返回的事件,不需要我们传入。由Linux内核设置具体的返回事件 */ 
}; fd是要监视的文件描述符,如果fd无效的话那么events监视事件也就无效,并且revents返回0。events是要监视的事件,可监视的事件类型如下所示:POLLIN   有数据可以读取。 POLLPRI   有紧急的数据需要读取。 POLLOUT  可以写数据。 POLLERR  指定的文件描述符发生错误。 POLLHUP  指定的文件描述符挂起。 POLLNVAL  无效的请求。 POLLRDNORM 等同于POLLIN 

《开发指南》中给出的示例代码:

// 示例代码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 } 

1.3.3 epoll函数(不是重点,视频里也没有)

       传统的selcet和poll函数会随着所监听的fd数量的增加而率降低,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,很浪费时间。为此,epoll用于处理大并发,一般常在网络编程中使用epoll函数。
       当涉及的文件描述符fd比少时就适合用selcet和poll,本章就主要使用select和poll。

1.3.3.1 创建epoll句柄
int epoll_create(int size)
// size  : 从Linux2.6.8开始此参数已经没有意义了,随便填写一个大于0的值就可以
// return: 返回epoll句柄;若返回-1表示创建失败
1.3.3.2 添加监视
int epoll_ctl(int epfd,                  // 要操作的epoll句柄(就是epoll_create()返回的东西)int op,                    // 表示要对epfd(epoll句柄)进行的操作int fd,                    // 要监视的文件描述符struct epoll_event *event) // 要监视的事件类型// return:0,成功;-1,失败,并且设置errno的值为相应的错误码
其中op参数可以是:
EPOLL_CTL_ADD  向句柄添加文件参数fd表示的描述符 
EPOLL_CTL_MOD  修改参数fd的event事件
EPOLL_CTL_DEL  从epfd中删除fd描述符

        参数中的epoll_event结构体定义如下:

struct epoll_event { uint32_t     events; /* epoll事件 */ epoll_data_t  data;  /* 用户数据 */ 
}; 其中events的值可以是:(也可以进行或操作同时监视多个事件)
EPOLLIN   有数据可以读
EPOLLOUT  可以写
EPOLLPRI  有紧急数据需要读
EPOLLERR  指定的文件描述符发生错误
EPOLLHUP  指定的文件描述符挂起 
EPOLLET   设置epoll为边沿触发,默认为水平触发
EPOLLONESHOT 一次性的监视。当监视完成以后,若还需要监视某个fd,需要将fd重新添加到epoll里
1.3.3.3 等待

        设置好以后应用程序就可以通过epoll_wait函数来等待事件的发生。

int epoll_wait(int epfd,       // 要等待的epollstruct epoll_event  *events, // 指向epoll_event结构体的数组int maxevents,  // events数组大小,必须大于0int timeout     // 超时时间(ms)
)                              // return: 0,超时;-1,错误;其他值,准备就绪的文件描述符数量

1.3.4 Linux驱动下的poll

        使用select或poll时,需要在驱动程序file_operations操作集中添加poll函数。当应用程序调用select或poll来对驱动程序进行非阻塞访问的时候,就会执行file_operations中的poll函数。

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

示例:

// 这段代码来自drivers/media/pci/ttpci/av7110_ca.c
static const struct file_operations dvb_ca_fops = {………….poll		= dvb_ca_poll,…………
};static unsigned int dvb_ca_poll (struct file *file, poll_table *wait){……struct dvb_ringbuffer *rbuf = &av7110->ci_rbuffer;struct dvb_ringbuffer *wbuf = &av7110->ci_wbuffer;……poll_wait(file, &rbuf->queue, wait); // rbuf->queue即是对应的等待队列头poll_wait(file, &wbuf->queue, wait);……
}

2、实验

2.1 阻塞式IO

2.1.1 文件结构

15_BLOCKIO (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 15_blockio.code-workspace
├── Makefile
├── blockio.c
└── irqAPP.c

2.1.2 Makefile

CFLAGS_MODULE += -wKERNELDIR := /....../linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子“01、例程源码”中直接搜,cp到虚拟机里面)CURRENT_PATH := $(shell pwd)	# 当前路径obj-m := blockio.o			# 编译文件build: kernel_modules			# 编译模块kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

2.1.3 blockio.c

直接在上次中断的驱动代码irq.c基础上改即可。修改了以下部分:

        在设备结构体中                    增加了等待队列头

        在驱动入口函数key_init()中  增加了等待队列头初始化

        在读操作函数irq_read()中     增加了等待事件,等待按键有效

        在定时器处理函数timer_func增加了唤醒进程

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    #include <linux/io.h>
    #include <linux/cdev.h>
    #include <linux/kdev_t.h>
    #include <linux/stat.h>
    #include <linux/device.h>
    #include <linux/of_gpio.h>
    #include <linux/timer.h>
    #include <linux/interrupt.h>
    #include <linux/atomic.h>
    #include <linux/wait.h>
    #include <linux/ide.h>#define DEV_CNT  1          /* 设备号数量 */
    #define DEV_NAME "irq"      /* 设备名 */
    #define KEY_NUM 1           /* 按键数量 */
    #define KEY0_VALUE 0x01     /* 按键值(假定释放为1,按下为0)*/
    #define INVAKEY    0x00     /* 按键无效值 *//* 按键结构体 */
    struct irq_keydesc{int gpio;   /* io号 */int irqnum; /* 中断号 */unsigned char value; /* 键值 (默认/空闲电平,比如 1) */char name[10];  /* 名字 */irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/// struct tasklet_struct tasklet;struct work_struct work;
    };/* 中断设备结构体 */
    struct irq{dev_t devid;    // 设备号int major;      // 主设备号int minor;      // 次struct cdev cdev;       // cdevstruct device *device;  // 设备struct class *class;    // 类struct device_node *nd; // 节点struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键struct timer_list timer;// 定时器atomic_t releasekey;  // 0表示按键释放,1表示按键按下atomic_t keyvalue;    // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取// 其余七位表示按键的值wait_queue_head_t r_wait; //读 等待队列头
    };
    static struct irq key_irq;/* 打开/读操作 */
    static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
    }
    static ssize_t irq_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 irq* dev = filp->private_data;wait_event_interruptible(dev->r_wait, (atomic_read(&dev->keyvalue) & 0x80)); // 等待按键有效keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按键释放,此时可以读取数据if(keyvalue & 0x80){  // 如果最高位为1,表示此时数据还未被读取unsigned char out = keyvalue & 0x7F; // 将最高位置0(最高位为有效位,低7位为实际value)atomic_set(&dev->keyvalue, 0);  // 清零,表示数据已被读取atomic_set(&dev->releasekey, 0);if(copy_to_user(buf, &out, sizeof(out))){            // 返回按键值给用户态return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 释放标志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
    }/* 操作集 */
    static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
    };/* 中断处理函数 */
    static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level;  // 保存中断时的电平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level);  // 保存基准电平// tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
    }/* 定时器处理函数 */
    static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定时器延时后的电平/* 读取当前按键电平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若当前电平与中断发生时读取到的电平一致,则有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 释放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1);  // 表示已释放,此时用户态可读} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下// atomic_set(&dev->releasekey, 0);  // 表示未释放}}// 唤醒进程if(atomic_read(&dev->releasekey)){wake_up(&dev->r_wait);}
    }/* tasklet处理函数 */
    static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk("      key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */}
    /* 工作队列处理函数 */
    static void key_work(struct work_struct *work_){printk("      key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */// struct irq* dev = container_of(work_, struct irq, work);
    }/* 初始化按键 */
    static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按键初始化 */dev->nd = of_find_node_by_path("/key");  // 获取设备节点if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio);  // 设置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中断处理函数dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高电平),0 表示按下(低电平)/* 2. 中断初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 传入结构体指针 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}// tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
    fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
    fail_nd:return ret;
    }/* 驱动入口 */
    static int __init key__init(void){int ret = 0;/* 1. 注册字符设备驱动 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 创建类 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 创建设备 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定时器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 *//* 8. 初始化原子变量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);/* 9.初始化等待队列头 */init_waitqueue_head(&key_irq.r_wait);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
    fail_device:class_destroy(key_irq.class);
    fail_class:cdev_del(&key_irq.cdev);
    fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
    }/* 驱动出口 */
    static void __exit key_exit(void){int i=0;/* 释放中断 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 释放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 删除定时器 */del_timer_sync(&key_irq.timer);/* 注销字符设备驱动 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
    }module_init(key__init);
    module_exit(key_exit);
    MODULE_LICENSE("GPL");
    

    2.1.4 irqApp

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<sys/ioctl.h>
    #include<fcntl.h>
    #include<stdlib.h>
    #include<string.h>/* * @description    : main主程序 * @param - argc   : argv数组元素个数 * @param - argv   : 具体参数 * @return         : 0 成功; else失败* 调用  ./irqAPP /dev/timer*/ #define LEDOFF 0
    #define LEDON  1int main(int argc, char *argv[]){if(argc != 2){  // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;filename = argv[1];fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filenameif(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {ret = read(fd, &data, sizeof(data));if (ret > 0) {printf("key value = %#x\r\n", data);}}close(fd);return 0;
    }

    2.1.5 测试

    先运行上一次中断实验的驱动,看一眼CPU占用,直接被吓昏:

    然后再测试这次的实验代码:

    # VSCODE终端
    make
    arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
    sudo cp blockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
    cd /lib/modules/4.1.15/
    depmod
    modprobe blockio.ko
    ./irqAPP /dev/irq &  # 后台启动应用程序
    top            # 查看CPU占用,此时占用应当很低# 此时按键应当有输出
    kill -9 XXX    # 关闭后台应用程序

    使用阻塞式以后就不会有占用CPU的问题:

    2.2 非阻塞式IO

    2.2.1 文件结构

            与阻塞式完全一致。

    16_NOBLOCKIO (工作区)
    ├── .vscode
    │   ├── c_cpp_properties.json
    │   └── settings.json
    ├── 16_noblockio.code-workspace
    ├── Makefile
    ├── noblockio.c
    └── irqAPP.c

    2.2.2 Makefile

            与阻塞式完全一致。修改一下obj-m项即可。

    2.2.3 noblockio.c

    基本和阻塞式一致,只在几个地方有修改。

    更新:

            修改了read函数

            增加了头文件#include <linux/poll.h>

            增加了操作集file_operations中的.poll以及对应的irq_poll()函数

      #include <linux/module.h>
      #include <linux/kernel.h>
      #include <linux/init.h>
      #include <linux/fs.h>
      #include <linux/uaccess.h>
      #include <linux/io.h>
      #include <linux/cdev.h>
      #include <linux/kdev_t.h>
      #include <linux/stat.h>
      #include <linux/device.h>
      #include <linux/of_gpio.h>
      #include <linux/timer.h>
      #include <linux/interrupt.h>
      #include <linux/atomic.h>
      #include <linux/wait.h>
      #include <linux/ide.h>
      #include <linux/poll.h>#define DEV_CNT  1          /* 设备号数量 */
      #define DEV_NAME "irq"      /* 设备名 */
      #define KEY_NUM 1           /* 按键数量 */
      #define KEY0_VALUE 0x01     /* 按键值(假定释放为1,按下为0)*/
      #define INVAKEY    0x00     /* 按键无效值 *//* 按键结构体 */
      struct irq_keydesc{int gpio;   /* io号 */int irqnum; /* 中断号 */unsigned char value; /* 键值 (默认/空闲电平,比如 1) */char name[10];  /* 名字 */irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/// struct tasklet_struct tasklet;struct work_struct work;
      };/* 中断设备结构体 */
      struct irq{dev_t devid;    // 设备号int major;      // 主设备号int minor;      // 次struct cdev cdev;       // cdevstruct device *device;  // 设备struct class *class;    // 类struct device_node *nd; // 节点struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键struct timer_list timer;// 定时器atomic_t releasekey;  // 0表示按键释放,1表示按键按下atomic_t keyvalue;    // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取// 其余七位表示按键的值wait_queue_head_t r_wait; //读 等待队列头
      };
      static struct irq key_irq;/* 打开/读操作 */
      static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
      }
      static ssize_t irq_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 irq* dev = filp->private_data;if(filp->f_flags & O_NONBLOCK){ // 非阻塞方式if(atomic_read(&dev->releasekey) == 0){ // 无效return -EAGAIN;}}else{ // 阻塞方式wait_event_interruptible(dev->r_wait, (atomic_read(&dev->keyvalue) & 0x80)); // 等待按键有效}keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)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;}data_error:ret = -EINVAL;return ret;
      }// 应用程序先调用poll,查看是否可读。
      // 如果可读,则调用read函数
      static unsigned int irq_poll(struct file *filp, struct poll_table * wait){int ret = 0;struct irq *dev = filp->private_data;poll_wait(filp, &dev->r_wait, wait);// 是否可读if(atomic_read(&dev->releasekey)){ // 按键按下,可读ret = POLLIN | POLLRDNORM; // 表示有数据可以读。详见1.3.2部分}return ret;
      }/* 操作集 */
      static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,.poll = irq_poll,  // 详见1.3.4
      };/* 中断处理函数 */
      static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level;  // 保存中断时的电平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level);  // 保存基准电平// tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
      }/* 定时器处理函数 */
      static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定时器延时后的电平/* 读取当前按键电平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若当前电平与中断发生时读取到的电平一致,则有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 释放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1);  // 表示已释放,此时用户态可读} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下// atomic_set(&dev->releasekey, 0);  // 表示未释放}}// 唤醒进程if(atomic_read(&dev->releasekey)){wake_up(&dev->r_wait);}
      }/* 工作队列处理函数 */
      static void key_work(struct work_struct *work_){printk("      key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */// struct irq* dev = container_of(work_, struct irq, work);
      }/* 初始化按键 */
      static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按键初始化 */dev->nd = of_find_node_by_path("/key");  // 获取设备节点if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio);  // 设置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中断处理函数dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高电平),0 表示按下(低电平)/* 2. 中断初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 传入结构体指针 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
      fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
      fail_nd:return ret;
      }/* 驱动入口 */
      static int __init key__init(void){int ret = 0;/* 1. 注册字符设备驱动 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 创建类 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 创建设备 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定时器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 *//* 8. 初始化原子变量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);/* 9.初始化等待队列头 */init_waitqueue_head(&key_irq.r_wait);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
      fail_device:class_destroy(key_irq.class);
      fail_class:cdev_del(&key_irq.cdev);
      fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
      }/* 驱动出口 */
      static void __exit key_exit(void){int i=0;/* 释放中断 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 释放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 删除定时器 */del_timer_sync(&key_irq.timer);/* 注销字符设备驱动 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
      }module_init(key__init);
      module_exit(key_exit);
      MODULE_LICENSE("GPL");
      

      2.2.4 用户应用程序

      2.2.4.1 使用select实现非阻塞

      代码过程:

      1. 用户态应用程序调用select。内核会检查文件描述符fd是否就绪(是否有按键事件)
      2. 如果不就绪,内核将进程加入等待队列r_wait并挂起(TASK_INTERRUPTIBLE状态),CPU会去执行其他任务。
      3. 当内核事件发生时:(按键中断)
            中断处理函数key0_handler调度工作队列schedule_work
            工作队列处理函数key_work启动定时器mod_timer
            执行定时器处理函数timer_func
            内核唤醒用户进程,select返回,用户进程执行read读取数据
      #include<stdio.h>
      #include<unistd.h>
      #include<sys/types.h>
      #include<sys/time.h>
      #include<fcntl.h>
      #include<sys/select.h>/* * @description    : main主程序 * @param - argc   : argv数组元素个数 * @param - argv   : 具体参数 * @return         : 0 成功; else失败* 调用  ./irqAPP /dev/timer*/ #define LEDOFF 0
      #define LEDON  1int main(int argc, char *argv[]){if(argc != 2){  // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;fd_set readfds; // 监视指定描述符集的读变化 详见1.3.1.2struct timeval timeout; // 超时时间      详见1.3.1.3filename = argv[1];fd = open(filename, O_RDWR | O_NONBLOCK);  // 非阻塞方式打开if(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {// 构建fds// timeout.tv_sec = 0;  // 需要#include <sys/time.h>// timeout.tv_usec = 500000; // 设置超时时间500mstimeout.tv_sec = 1;  // 设置超时时间1stimeout.tv_usec = 0; // FD_ZERO(&readfds);      // 清零FD_SET(fd, &readfds);   // 要监视的文件描述符的集合ret = select(fd+1, &readfds, NULL,     NULL,    &timeout);//参数为: nfds  readfds  writefds  exceptfds  timeoutswitch(ret){  // 返回参数详见1.3.1.4case 0: // 超时printf("select timeout!\r\n");break;case -1:// 错误break;default:// 可以读取数据if(FD_ISSET(fd,&readfds)){ // 如果是fd文件描述符,则调用readret = read(fd, &data, sizeof(data)); // 与阻塞式read部分代码一致if (ret > 0) {printf("key value = %#x\r\n", data);}}break;}}printf("App finished!\r\n");close(fd);return 0;
      }
       测试
      # VSCODE
      make
      arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
      sudo cp noblockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
      rmmod blockio.ko 首先卸载之前的阻塞式代码ps
      kill -9 XXX    # 关掉之前阻塞式的应用程序/dev/irqAPPmodprobe noblockio.ko
      ./irqAPP /dev/time

      可以看到基本没有CPU占用;如果没有动作,会每秒触发一次timeout;按键能正常触发

      2.2.4.2 使用poll函数实现非阻塞

              前面提到,select函数能够监视的文件描述符数量有限制。我们还可以使用poll函数。

      #include<stdio.h>
      #include<unistd.h>
      #include<sys/types.h>
      #include<sys/time.h>
      #include<fcntl.h>
      #include<sys/select.h>
      #include<poll.h>
      #include<signal.h>/* * @description    : main主程序 * @param - argc   : argv数组元素个数 * @param - argv   : 具体参数 * @return         : 0 成功; else失败* 调用  ./irqAPP /dev/timer*/ #define LEDOFF 0
      #define LEDON  1int main(int argc, char *argv[]){if(argc != 2){  // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;// fd_set readfds;struct pollfd fds;struct timeval timeout; // 超时时间      详见1.3.1.3filename = argv[1];fd = open(filename, O_RDWR | O_NONBLOCK);  // 非阻塞方式打开if(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {fds.fd = fd; // 文件描述符fds.events = POLLIN; // 监视的事件:有数据可以读取时触发ret = poll(&fds, 1, 500); //参数:要监视的文件描述符集合以及要监视的事件,函数要监视的文件描述符数量,500ms超时if(ret==0){ // 超时}else if(ret < 0){ // 错误}else{ // 正常if(fds.revents | POLLIN){ // 可读取ret=read(fd, &data, sizeof(data));if(ret < 0){}else{if(data){printf("key value = %#x\r\n",data);}}}}}printf("App finished!\r\n");close(fd);return 0;
      }
      测试

              驱动程序和select使用的驱动完全一致。直接挂载驱动并执行应用程序:

      # VSCODE
      make
      arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
      sudo cp noblockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
      rmmod blockio.ko 首先卸载之前的阻塞式代码ps
      kill -9 XXX    # 关掉之前阻塞式的应用程序/dev/irqAPPmodprobe noblockio.ko
      ./irqAPP /dev/time

      http://www.dtcms.com/a/511195.html

      相关文章:

    1. Docker----快速入门
    2. 深度学习8-卷积神经网络-CNN概述-卷积层-池化层-深度卷积神经网络-案例:服装分类
    3. 厦门做外贸网站国内十大咨询公司排名
    4. 架构设计过去十年与未来十年
    5. Nginx 日志轮转
    6. 《Linux运维总结:基于ARM64+X86_64架构CPU使用docker-compose一键离线部署mongodb 7.0.22容器版副本集群》
    7. 《Linux运维总结:基于ARM64+X86_64架构CPU使用docker-compose一键离线部署mongodb 7.0.22容器版分片集群》
    8. MongoDB基础与Mongoose ODM
    9. 做定制网站价格教做flash的网站
    10. 【流量控制】算不对 GBN 窗口?分不清 SR 重传?滑动窗口 + 3 大协议一篇吃透
    11. 临时插入的紧急任务如何影响整体进度
    12. 国内net开发的网站建设网站建设费如何会计处理
    13. Melos 使用指南:Flutter / Dart 多包管理工具!
    14. React组件完全指南
    15. TypeScript:npm的types、typings、@type的区别
    16. 我的第一份开源贡献:小米工程师程赛的社区之旅
    17. Python 基础 | 第八课:函数详解与应用
    18. 火狐浏览器替换js脚本
    19. 车载诊断架构 --- 由一个售后问题引发对P4时间的思考
    20. 第3章 SQL数据定义语句
    21. phpcms 网站m8 wordpress主题
    22. Docker到Kubernetes的平滑迁移(服务网格实战)
    23. 数据挖掘知识体系分析
    24. 简述网站建设的五类成员做电商网站公司
    25. 数据结构——邻接表
    26. 预算系统 - 项目优化点
    27. 【软考备考】论软件架构设计-范文示例
    28. 探讨一下java将来未来两年内的就业以及发展
    29. [特殊字符] 已发布目标检测数据集合集(持续更新)
    30. mysql主从延迟