这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 软件与操作系统 » Linux驱动程序可用的内核辅助工具(一)

共3条 1/1 1 跳转至

Linux驱动程序可用的内核辅助工具(一)

高工
2023-09-22 09:57:09     打赏

Linux 内核是独立的软件,他没有使用任何 C 语言库,他自己实现了很多工具和辅助工具。

本系列文章将盘点一些内核提供的辅助工具函数。在编写驱动程序时,我们可以利用内核提供的工具函数,方便实现目标功能。

宏 container_of

这个宏定义非常出名,好多文章对齐进行了解析,并且这个宏在内核和驱动中经常见到。

该宏的作用是通过结构体成员的地址和结构体类型推导出结构体的地址。

在 linux 源码的 tools\include\linux\kernel.h文件下,container_of()的定义如下:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({   \
  const typeof(((type *)0)->member) * __mptr = (ptr); \
  (type *)((char *)__mptr - offsetof(type, member)); })

宏的参数分别为:type 是指结构体的类型,member 是成员在结构体中的名字,ptr 是该成员在 type 结构体中的地址。

宏container_of主要用在内核的通用容器中 。

该宏的详细介绍可以参考:对linux内核中container_of()宏的理解

链表

链表有两种类型:

  • 单向链表
  • 双向链表

内核中实现了循环双向链表,这个结构能够实现FIFO和LIFO。如果要使用内核提供的链表操作函数,代码中需要添加头文件 <linux/lis.h>。

内核中链表的实现核心部分数据结构 struct list_head 定义为:

struct list_head
{
 struct list_head *next, *prev;
}

struct list_head数据结构不包含链表节点的数据区,通常是用在链表头或者嵌入到其他数据结构中。

创建和初始化链表有两种方法:动态创建和静态创建。

动态方法创建并初始化链表方法如下:

struct list_head mylist;
INIT_LIST_HEAD(&mylist);

INIT_LIST_HEAD() 展开如下:

static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

静态创建链表通过 LIST_HEAD 宏完成:

LIST_HEAD(mylist)

LIST_HEAD 的定义如下:

#define LIST_HEAD(name) \
   struct list_head name = LIST_HEAD_INIT(name)

其中LIST_HEAD_INIT 展开为:

#define LIST_HEAD_INIT(name) { &(name), &(name) }

把next和prev指针都初始化并指向自己,这样便初始化了一个带头节点的空链表。

添加节点到链表中,内核提供了几个接口函数,如list_add()是把一个节点添加到表头,list_add_tail()是插入表尾。

void list_add(struct list_head *new, struct list_head *head)
list_add_tail(struct list_head *new, struct list_head *head)

内核提供的list_add用于向链表添加新项,它是内部函数__list_add的包装。

static inline void __list_add(struct list_head *new, struct list_head *prev,struct list_head *next)
{
  next->prev = new;
  new->next = next;
  new->prev = prev;
  prev->next = new;
}

删除节点很简单:

void list_del(struct list_head *entry);

链表遍历

使用宏 list_for_each_entry(pos, head, member) 进行链表遍历。

参数解释 head:链表的头节点;member:数据结构中链表 struct list_head 的名称;pos:用于迭代。它是一个循环游标,就像 for(i=0; i<foo; i++) 中的 i 。

#define list_for_each_entry(pos, head, member) \ 
for (pos = list_entry((head)->next,typeof(*pos), member); \
  &pos->member != (head); \
   pos = list_entry(pos->member.next,typeof(*pos), member))

#define list_entry(ptr, type, member)  container_of(ptr, type, member)
内核的睡眠机制

内核调度器管理要运行的任务列表,这被称作运行队列。睡眠进程不再被调度,因为已将它们从运行队列 中移除。除非其状态改变(唤醒),否则睡眠进程将永远不会被执行。

进程一旦进入等待状态,就可以释放处理器,一定要确保有条件或其他进程会唤醒它。Linux 内核通过提供一组函数和数据结构来简化睡眠机制的实现 。

等待队列

Linux内核提供了一个数据结构,用来记录等待执行的任务,那就是等待队列,主要用于处理被阻塞的 I/O 操作。其结构定义在 include/linux/wait.h 文件中:

struct wait_queue_entry {
 unsigned int  flags;
 void   *private;
 wait_queue_func_t func;
 struct list_head entry;
};

其中,entry  字段是一个链表,将进入睡眠的进程加入到这个链表中(在链表中排队),并进入睡眠状态。

处理等待队列也有两种方式:静态声明、动态声明,常用到的函数如下:

  • 静态声明
DECLARE_WAIT_QUEUE_HEAD(name)
  • 动态声明
wait_queue_head_t my_wait_queue;

init_waitqueue_head(&my_wait_queue);
  • 阻塞某个任务
/* 如果条件condition为真,则唤醒任务并执行。若为假,则阻塞 */
wait_event_interruptible(wq_head, condition)
  • 解除阻塞
void wake_up_interruptible(wait_queue_head_ts *q)

wait_event_interruptible 不会持续轮询,而只是在被调用时评估条件。如果条件为假,则进程将进入TASK_INTERRUPTIBLE 状态并从运行队列中删除。

当每次在等待队列中调用 wake_up_interruptible 时,都会重新检查条件。如果 wake_up_interruptible 运行时发现条件为真,则等待队列中的进程将被唤醒,并将其状态设置为 TASK_RUNNING。

进程按照它们进入睡眠的顺序唤醒。要唤醒在队列中等待的所有进程,应该使用 wake_up_interruptible_all。

如果调用了 wake_up 或 wake_up_interruptible,并且条件仍然是 FALSE,则什么都不会发生。如果没有调 用 wake_up(或 wake_up_interuptible ),进程将永远不会被唤醒 。

工作队列

等待队列有了,Linux 内核提供了工作队列,其中的 work 结构定义如下

struct work_struct {
 atomic_long_t data;
 struct list_head entry;
 work_func_t func;
#ifdef CONFIG_LOCKDEP
 struct lockdep_map lockdep_map;
#endif
};

其中,func 为工作 work 的处理函数,其类型定义为:

typedef void (*work_func_t)(struct work_struct *work);

Linux 内核一直运行着 worker 线程,他会对工作队列中的 work 进行处理。

定义并初始化一个 work 操作如下:

struct work_struct wrk;

INIT_WORK(_work, _func)

将work 添加进内核的全局工作队列中,即让 work 参与调度

schedule_work(struct work_struct *work)

在驱动中,工作队列和等待队列可以配合使用。

定时器

Linux 内核提供了两种定时器:

  • 标准定时器
  • 高精度定时器

下边分别进行介绍。

标准定时器

标准定时器是以 jffies  为基本单位计数。jiffy 是在 <linux/jiffies.h> 中声明的内核时间单位。

jffies 是记录着从电脑开机到现在总共的时钟中断次数。取决于系统的时钟频率,单位是 Hz,一般是一秒钟中断产生的次数,每个增量被称为一个 Tick(时钟节拍)。

内核中定时器的结构定义为,在文件<linux/timer.h> 中:

struct timer_list {
 struct hlist_node entry;
 unsigned long  expires;
 void   (*function)(struct timer_list *);
 u32   flags;

#ifdef CONFIG_LOCKDEP
 struct lockdep_map lockdep_map;
#endif
};

expires 是以 jiffies 为单位的绝对值。entry 是双向链表,function 为定时器的回调函数;flags 是可选的,被传递给回调函数。

设置定时器,提供用户定义的回调函数和标志变量值:

timer_setup(timer, callback, flags)

设置定时器的超时时间

mod_timer(struct timer_list *timer, unsigned long expires)

删除定时器:

void del_timer(struct timer_list *timer)

高精度定时器

内核 V2.6.16 引入了高精度定时器,通过配置内核 CONFIG_HIGH_RES_TIMERS 选项启用,其精度取决于平台,最高可达纳秒精度。标准定时器的精度为毫秒。

在系统上使用HRT时,要确认内核和硬件支持它。换句话说,必须用与平台相关的代码来访问硬件HRT。

若要使用高精度定时器,需要包含头文件 <linux/hrtimer.h>

Linux内核源码HRT结构定义如下:

struct hrtimer {
 struct timerqueue_node  node;
 ktime_t    _softexpires;
 enum hrtimer_restart  (*function)(struct hrtimer *);
 struct hrtimer_clock_base *base;
 u8    state;
 u8    is_rel;
 u8    is_soft;
 u8    is_hard;
};

HRT初始化操作

void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);

启动 hrtimer

void hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)

其中,mode 代表到期模式。对于绝对时间值,它应该是 HRTIMER_MODE_ABS,对于相对于现在的时间值,应该是HRTIMER_MODE_REL。

取消 hrtimer

int hrtimer_cancel( struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer)

这两个函数当定时器没被激活时都返回 0,激活时 返回1。这两个函数之间的区别是,如果定时器处于激活状态或其回调函数正在运行,则 hrtimer_try_to_cancel 会失败,返回-1,而 hrtimer_cancel 将等待回调完成。

内核内部维护着一个任务超时列表(它知道什么时候要睡眠以及睡眠多久)。

在空闲状态下,如果下一个Tick比任务列表超时中的最小超时更远,内核则使用该超时值对定时器进行编程。当定时器到期时,内核重新启用周期Tick并调用调度器,它调度与超时相关的任务 。

内核锁机制

设备驱动程序常用的锁有两种:

  • 互斥锁
  • 自旋锁

下面分别进行介绍。

互斥锁

互斥锁 mutex 是较常用的锁机制。他的结构在文件 include/linux/mutex.h 定义

struct mutex 
{
 atomic_long_t  owner;
 raw_spinlock_t  wait_lock;
 struct list_head wait_list;
 ...
};

wait_list 为等待互斥锁的任务链表。

静态声明互斥锁:

DEFINE_MUTEX(mutexname)

动态声明:

struct mutex my_mutex;
mutex_init(&my_mutex);

获取互斥锁:

void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);

释放互斥锁:

void mutex_unlock(struct mutex *lock);

调用 mutex_lock() 时要非常小心,只有能够保证无论在什么情况下互斥锁都会释放时才可以使用它。

在用户上下文中,建议始终使用 mutex_lock_interruptible() 来获取互斥锁,因为 mutex_lock()即使收到信号(甚至是Ctrl+C组合键),也不会返回。

互斥锁使用规则

使用互斥锁需要遵守一些规则:

  • 一次只能有一个任务持有互斥锁;
  • 多次解锁是不允许的。
  • 它们必须通过API初始化。
  • 持有互斥锁的任务不可能退出,因为互斥锁将保持锁定,可能的竞争者会永远等待(将睡眠)。
  • 不能释放锁定的内存区域。
  • 持有的互斥锁不得重新初始化。
  • 由于它们涉及重新调度,因此互斥锁不能用在原子上下文中,如 Tasklet 和定时器。

自旋锁

自旋锁,顾名思义,就是CPU一直在循环等待锁可以获取。因此,线程在获取自旋锁的过程中会大量消耗CPU。

因此在可以快速获取时再使用它,尤其是当持有自旋锁的时间比重新调度时间少时 。一旦关键任务完成,自旋 锁就应该被释放。

在一个处理器上,自旋意味着在该处理器上不能再运行其他任何任务;因此,在单核机器上使用自旋锁是没有任何意义的。最佳情况下,系统可能会变慢,最糟情况下,和互斥锁一样会造成死锁。

在单个处理器(核)系统上,应该使用下边两种接口函数

spin_lock_irqsave(lock, flags)
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

它们分别禁用处理器上中断,防止中断并发。

由于事先并不知道所写驱动程序运行在什么系统上,因此建议使用 spin_lock_irqsave () 获取自旋锁,该函数会 在获取自旋锁之前,禁止当前处理器(调用该函数的处理器)上中断。

然后,应该用 spin_unlock_irqrestore() 释放锁,它执行的操作与获取自旋锁操作相反。

自旋锁与互斥锁

自旋锁和互斥锁都用用于处理内核中并发访问,它们有各自的使用场景。

两种锁有以下几点区别:

  • 互斥锁保护进程的关键资源,而自旋锁保护IRQ处理程序的关键部分 。
  • 互斥锁让竞争者在获得锁之前睡眠,而自旋锁在获得锁之前一直自旋循环(消耗CPU)。
  • 自旋锁不能长时间持有,因为等待者在等待取锁期间会浪费CPU时间;而互斥锁则可以长时间持有,因为竞争者被放入等待队列中进入睡眠状态。

好了,今天就这些内容。





关键词: Linux     驱动程序     内核     辅助工具    

高工
2023-10-11 08:32:10     打赏
2楼

LINUX学习的东东很多


高工
2023-10-18 08:37:43     打赏
3楼

知识点密集且复杂


共3条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]