Linux内核定时器、阻塞_非阻塞IO

一.内核时间管理

Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz,100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。

设置好以后打开 Linux 内核源码根目录下的.config 文件,在此文件中有如图定义

Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。打开文件 include/asm-generic/param.h,有如下内容:

#undef  HZ
#define HZ    CONFIG_HZ
#define USER_HZ    100
#define  CLOCKS_PER_SEC    (USER_HZ)

这里定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ=100,我们后面编写 Linux驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:

extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

第 1行,定义了一个 64 位的 jiffies_64。
第 2行,定义了一个 unsigned long 类型的 32 位的 jiffies。
jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。为了兼容不同的硬件,jiffies 其实就是 jiffies_64 的低 32 位。
不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候,32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要,Linux 内核提供了如表 所示的几个 API 函数来处理绕回。

如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。 time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:

二.内核定时器简介

Linux 内核定时器采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件include/linux/timer.h 中,定义如下

struct timer_list {
struct list_head     entry;
unsigned long        expires;    /* 定时器超时时间,单位是节拍数 */
struct tvec_base     *base; 
void     (*function)(unsigned long); /* 定时处理函数 */
unsigned long         data; /* 要传递给 function 函数的参数 */
int slack;
};

要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器,tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。

1.init_timer 函数

init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下。init_timer 函数原型如下:

void init_timer(struct timer_list *timer)

timer:要初始化定时器。
返回值:没有返回值。

2.add_timer 函数

add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:

void add_timer(struct timer_list *timer)

timer:要注册的定时器。
返回值:没有返回值。

3.del_timer 函数

del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:

int del_timer(struct timer_list * timer)

timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活。

4.del_timer_sync 函数

del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:

int del_timer_sync(struct timer_list *timer)

timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活

5.mod_timer 函数

mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器!函数原型如下:

int mod_timer(struct timer_list *timer, unsigned long expires)

timer:要修改超时时间(定时值)的定时器。
expires:修改后的超时时间。
返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。
示例代码:


内核短延时函数

三.阻塞和非阻塞IO

这里的“IO”并不是我们学习 STM32 或者其他单片机的时候所说的“GPIO” (也就是引脚)。这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
应用程序可以使用如下所示示例代码来实现阻塞访问

对于设备驱动文件的默认读取方式就是阻塞式的,所以我们前面所有的例程测试 APP 都是采用阻塞 IO。
如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:

1.等待队列

1.1等待队列头

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

struct __wait_queue_head {
spinlock_t          lock;
struct list_head    task_list;
};
typedef struct __wait_queue_head   wait_queue_head_t;

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

void init_waitqueue_head(wait_queue_head_t *q)

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

1.2等待队列项

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

struct __wait_queue {
unsigned int    flags;
void            *private;
wait_queue_func_t  func;
struct list_head  task_list;
};
typedef struct __wait_queue wait_queue_t;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:

DECLARE_WAITQUEUE(name, tsk)

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

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

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

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

q:等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无
等待队列项移除 API 函数如下:

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

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

1.5等待事件

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

2.轮询

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

2.1 select 函数

select 函数原型如下:

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

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

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

2.2poll函数

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

2.3epoll函数

传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll应运而生,epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。

2.4 Linux驱动下的poll函数

当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数,poll 函数原型如下所示:

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

函数参数和返回值含义如下:
filp:要打开的设备文件(文件描述符)。
wait:结构体 poll_table_struct 类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:

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

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

相关文章

Bootstrap 滚动监听(Scrollspy)插件

滚动监听(Scrollspy)插件,即自动更新导航插件,会根据滚动条的位置自动更新对应的导航目标。其基本的实现是随着您的滚动,基于滚动条的位置向导航栏添加 .active class。 如果您想要单独引用该插件的功能,那…

【前端】文件上传框架plupload使用(前后端交互)

这个框架是用来给前端设置文件上传的按钮的。 首先要明白,前端向后端发送请求的方式有get和post,两者的区别在于,前者只能在网址中携带参数,后者是在请求体body中携带参数。 Plupload向后端发送请求是post请求方式,发送…

【Python】从基础到进阶(六):深入理解Python中的面向对象编程(OOP)

🔥 个人主页:空白诗 文章目录 一、引言二、面向对象编程概述1. 什么是面向对象编程?面向对象的三大基本特性 2. 类和对象3. 类的属性与方法 三、继承与多态1. 继承2. 多态 四、封装与数据隐藏1. 封装2. 数据隐藏 五、案例:员工管理…

华为自研仓颉编程语言测试版上线,计划持续到10月21号

现如今,编程语言作为构建软件世界的基石,其重要性不言而喻。 而华为,作为全球领先的信息与通信技术(ICT)解决方案提供商,其在技术创新上的每一步都备受瞩目。最近,华为再次成为焦点&#xff0c…

USB3.0硬件简单概述

关于USB2.0,小白在之前的文章简单的描述过。关于USB3.0,今天小白也简单的介绍下。 相比较于USB2.0,USB3.0有了很大的变化。信号端USB3.0除了包含USB2.0的四根信号,还新增了2对差分信号SSRXN SSRXP SSTXN SSTXP以及GND_DRAIN(信号…

Flask返回Json格式字符,中文导致unicode乱码问题

一.问题描述 或者直接返回json格式的字符串 从上图可以看出,当flask实现的接口响应中存在中文时,接口返回json字串的中文为unicode乱码。 二.问题解决 百度搜索了很多,原来在创建flask app时使用json格式的字符串,默认是ascii编…

金融科技 API 接口:提升金融服务效率的关键

金融科技是应用技术手段和创新理念来提升金融服务效率的重要途径。而其中的API接口则是实现金融科技的关键。API接口的简单定义是提供计算机程序之间通信的规范和工具,提供一种方法和数据的交互形式,以便开发人员能够利用现有的软件来创建新的应用和服务…

【区块链 + 航运物流】运力链 | FISCO BCOS应用案例

根据速达物联的战略规划,2023年物流平台将由单一调度平台升级为物流生态平台。基于此,虎彩集团采用 FISCO BCOS区块链技术构建的运力链,可以帮助客户实现资源广泛快速连接、合作伙伴间的高效协同和低摩擦交 易,最终达成可信同城货…

五、Centos7-安装Jenkins

目录 一、基础环境准备 1.安装JDK 2.安装Tomcat 二、安装Jenkins 1.配置Jenkins插件镜像源 2.问题:进入manager jenkins页面报错 3.配置Git 4.配置jdk 三、重新安装Jenkins 四、另一种Centos安装jenkins的方式--最终可用版 克隆了一个base的虚拟机&#x…

Upload-Lab第19关:用图片马和条件竞争技巧,轻松应对上传限制!

简介 在upload-labs的第19关中,攻击是通过上传图片马来实现的。与第18关相比,这一关增加了对文件后缀名的检测,禁止直接上传 PHP 文件。因此,你需要上传一个图片马,并使用文件包含漏洞来访问它。具体步骤与第18关类似。 这里有一个细节,由于可能是这个靶场的作者的某种原…

老师如何制作分班查询系统?

随着新学期的钟声敲响,老师们又迎来了一年中最忙碌的时期。不仅要处理日常的教学准备工作,还要面对一项重要而繁琐的任务——新生分班。分班完成后,老师们还需要将分班结果及时准确地通知给每一位家长,确保信息的传递无误。这项工…

p8 Run的流程和Docker原理

docker run的运行原理图 docker是怎么工作的? docker是一个cs的一个结构的系统docker的守护进程运行在宿主机上面通过socket进行访问 其实就是看下面的这个图,通过客户端的命令来操作docker的守护进程然后启动一些容器,默认容器是不启动的 …

语言基础/单向链表的构建和使用(含Linux中SLIST的解析和使用)

文章目录 概述简单的链表描述链表的术语简单实现一个单链表 Linux之SLIST机理分析结构定义单链表初始化单链表插入元素单链表遍历元素单链表删除元素 Linux之SLIST使用实践纯C中typedef重命名带来的问题预留 概述 本文讲述了数据结构中单链表的基本概念,头指针、头…

Go语言操作文件上传和下载应用教程

Go语言操作文件上传和下载应用教程 我们在使用Go的日常开发中,经常会遇到对文件的处理,例如:上传、下载、读写等(详情见Go 文件操作基本方法大全),且我们在实际应用中,基本都是使用框架自带的文…

电商项目DevOps一体化运维实战

主要讲了git和jkins的使用,其中maven的一个插件还挺好用的,主要可以用来查看哪些类没有使用,哪些导入的包是多余的等。这里展示一下用法。至于git和jkins的搭建后续再操作。 maven插件的使用: 编译后就可以在target下面看到这个h…

[ACL 2024] Revisiting Knowledge Distillation for Autoregressive Language Models

Contents IntroductionMethodRethinking Knowledge Distillation for Autoregressive LMsImproving Knowledge Distillation with Adaptive Teaching Modes ExperimentsReferences Introduction 作者提出 Autoregressive KD with Adaptive Teaching Modes (ATKD),通…

5000套精美PPT免费分享

目录 部分展示目录 几乎包含各种应用场景的PPT模板 这里只展示部分目录 部分展示目录 ##PPT下载 链接:https://pan.baidu.com/s/1ckvN9xeMR82hL30lHXfJ0g 提取码:ZYNB 点击下载,记得点个赞哦

3 pytest Fixture

目录 3.1 通过 conftest.py 共享 fixture3.2 使用 fixture 执行配置及销毁逻辑3.3 使用 --setup-show 回溯 fixture 的执行过程3.4 使用 fixture 传递测试数据3.5 使用多个 fixture3.6 指定 fixture 作用范围3.7 使用 usefixtures 指定 fixture3.8 为常用 fixture 添加 autouse…

MySQL从入门到精通(第9-10章)

文章目录 9 子查询9.1 需求分析与问题解决9.1.1 实际问题9.1.2 子查询的使用9.1.3 子查询的分类 9.2 单行子查询9.2.1 单行比较操作符9.2.2 代码示例9.2.3 HAVING中的子查询9.2.4 CASE中的子查询9.2.5 子查询中的空值问题9.2.6 非法使用子查询 9.3 多行子查询9.3.1 多行比较操作…

linux系统使用 docker 来部署web环境 nginx+php7.4 并配置称 docker-compose-mysql.yml 文件

Docker是一个开源的容器化平台,旨在简化应用程序的创建、部署和管理。它基于OS-level虚拟化技术,通过将应用程序和其依赖项打包到一个称为容器的标准化单元中,使得应用程序可以在任何环境中快速、可靠地运行。 Docker的优势有以下几个方面&a…