Skip to content

Latest commit

 

History

History
1681 lines (1203 loc) · 67 KB

File metadata and controls

1681 lines (1203 loc) · 67 KB

三、内核工具和助手函数

正如您将在本章中看到的,内核是一个独立的软件,它不使用任何 C 库。它实现了您在现代库中可能遇到的任何机制,甚至更多,例如压缩、字符串函数等。我们将逐步介绍这种能力的最重要方面。

在本章中,我们将讨论以下主题:

  • 介绍内核容器数据结构
  • 处理内核休眠机制
  • 使用计时器
  • 深入研究内核锁定机制(互斥、spnlock)
  • 使用内核专用的应用编程接口推迟工作
  • 使用 IRQs

理解宏的容器

当涉及到管理代码中的几个数据结构时,您几乎总是需要将一个结构嵌入到另一个结构中,并随时检索它们,而不会被问到关于内存偏移或边界的问题。假设你有一个struct person,如这里所定义的:

 struct person { 
     int  age; 
     char *name; 
 } p;

只要在agename上有一个指针,就可以检索包装(包含)该指针的整个结构。顾名思义,container_of宏用于查找结构中给定字段的容器。宏在include/linux/kernel.h中定义,如下所示:

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

不要害怕指针;就当它是:

container_of(pointer, container_type, container_field); 

下面是前面代码片段的元素:

  • pointer:这是指向结构中字段的指针
  • container_type:这是包装(包含)指针的结构类型
  • container_field:这是结构内部pointer指向的字段名称

让我们考虑以下容器:

struct person { 
     int  age; 
     char *name; 
 }; 

现在让我们考虑它的一个实例,以及一个指向name成员的指针:

struct person somebody; 
[...] 
char *the_name_ptr = somebody.name; 

除了指向name成员(the_name_ptr)的指针外,您还可以使用container_of宏,通过以下方式获取指向包装该成员的整个结构(容器)的指针:

struct person *the_person; 
the_person = container_of(the_name_ptr, struct person, name); 

container_of考虑结构开始处name的偏移量,得到正确的指针位置。如果您从指针the_name_ptr中减去字段name的偏移量,您将获得正确的位置。这是宏的最后一行:

(type *)( (char *)__mptr - offsetof(type,member) ); 

将此应用于一个真实的示例,它给出了以下内容:

struct family { 
    struct person *father; 
    struct person *mother; 
    int number_of_suns; 
    int salary; 
} f; 

/* 
 * pointer to a field of the structure 
 * (could be any member of any family) 
*/ 
struct *person = family.father; 
struct family *fam_ptr; 

/* now let us retrieve back its family */ 
fam_ptr = container_of(person, struct family, father); 

关于container_of宏,你只需要知道这些,相信我,就够了。在我们将在本书中进一步开发的真实驱动中,它看起来如下:

struct mcp23016 { 
    struct i2c_client *client; 
    struct gpio_chip chip; 
} 

/* retrive the mcp23016 struct given a pointer 'chip' field */ 
static inline struct mcp23016 *to_mcp23016(struct gpio_chip *gc) 
{ 
    return container_of(gc, struct mcp23016, chip); 
} 

static int mcp23016_probe(struct i2c_client *client, 
                const struct i2c_device_id *id) 
{ 
    struct mcp23016 *mcp; 
    [...] 
    mcp = devm_kzalloc(&client->dev, sizeof(*mcp), GFP_KERNEL); 
    if (!mcp) 
        return -ENOMEM; 
    [...] 
} 

controller_of宏主要用于内核中的泛型容器。在本书的一些例子中(从第五章平台设备驱动开始),你会遇到container_of宏。

合框架

假设您有一个管理多个设备的驱动,比如说五个设备。您可能需要在您的驱动中记录它们。这里需要的是一个链表。实际上存在两种类型的链表:

  • 简单链表
  • 双向链表

因此,内核开发人员只实现循环双链表,因为这种结构允许您实现先进先出和后进先出,并且内核开发人员注意维护最少的代码集。为了支持列表,代码中要添加的标题是<linux/list.h>。内核中列表实现的核心数据结构是struct list_head结构,定义如下:

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

struct list_head用于列表的头部和每个节点。在内核世界中,在一个数据结构可以表示为链表之前,该结构必须嵌入一个struct list_head字段。例如,让我们创建一个汽车列表:

struct car { 
    int door_number; 
    char *color; 
    char *model; 
}; 

在我们为汽车创建列表之前,我们必须改变它的结构,以便嵌入一个struct list_head字段。结构变成:

struct car { 
    int door_number; 
    char *color; 
    char *model; 
    struct list_head list; /* kernel's list structure */ 
}; 

首先,我们需要创建一个struct list_head变量,该变量将始终指向列表的头部(第一个元素)。list_head的这个实例与任何汽车都没有关联,并且是特殊的:

static LIST_HEAD(carlist) ; 

现在,我们可以创建汽车并将其添加到我们的列表中— carlist:

#include <linux/list.h> 

struct car *redcar = kmalloc(sizeof(*car), GFP_KERNEL); 
struct car *bluecar = kmalloc(sizeof(*car), GFP_KERNEL); 

/* Initialize each node's list entry */ 
INIT_LIST_HEAD(&bluecar->list); 
INIT_LIST_HEAD(&redcar->list); 

/* allocate memory for color and model field and fill every field */ 
 [...] 
list_add(&redcar->list, &carlist) ; 
list_add(&bluecar->list, &carlist) ; 

就这么简单。现在,carlist包含两个元素。让我们更深入地了解链表 API。

创建和初始化列表

有两种方法可以创建和初始化列表:

动力法

动态方法包括一个struct list_head并用INIT_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 s 的定义定义如下:

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

以下是它的扩展:

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

这将分配name字段中的每个指针(prevnext)指向name本身(就像INIT_LIST_HEAD一样)。

创建列表节点

要创建新节点,只需创建我们的数据结构实例,并初始化其嵌入的list_head字段。以汽车为例,它将给出以下内容:

struct car *blackcar = kzalloc(sizeof(struct car), GFP_KERNEL); 

/* non static initialization, since it is the embedded list field*/ 
INIT_LIST_HEAD(&blackcar->list); 

如前所述,使用INIT_LIST_HEAD,,这是一个动态分配的列表,通常是另一个结构的一部分。

添加列表节点

内核提供list_add向列表中添加一个新的条目,它是内部函数__list_add的包装器:

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

__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; 
} 

以下是在我们的列表中添加两辆汽车的示例:

list_add(&redcar->list, &carlist); 
list_add(&blue->list, &carlist); 

此模式可用于实现堆栈。向列表中添加条目的另一个功能是:

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

这将在列表末尾插入给定的新条目。给定我们之前的示例,我们可以使用以下内容:

list_add_tail(&redcar->list, &carlist); 
list_add_tail(&blue->list, &carlist); 

这种模式可用于实现队列。

从列表中删除节点

列表处理在内核代码中是一项简单的任务。删除节点很简单:

 void list_del(struct list_head *entry); 

按照前面的例子,让我们删除红色汽车:

list_del(&redcar->list); 

list_del disconnects the prev and next pointers of the given entry, resulting in an entry removal. The memory allocated for the node is not freed yet; you need to do that manually with kfree.

链表遍历

我们有用于列表遍历的宏list_for_each_entry(pos, head, member)

  • head是列表的头节点。
  • member是我们的数据结构中列表struct list_head的名称(在我们的例子中,它是list)。
  • pos用于迭代。它是一个循环光标(就像for(i=0; i<foo; i++)中的i)。head可以是链表的头节点,也可以是任何条目,我们不在乎,因为我们处理的是双重链表:
struct car *acar; /* loop counter */ 
int blue_car_num = 0; 

/* 'list' is the name of the list_head struct in our data structure */ 
list_for_each_entry(acar, carlist, list){ 
    if(acar->color == "blue") 
        blue_car_num++; 
} 

为什么我们的数据结构中需要list_head类型字段的名称?看看list_for_each_entry的定义:

#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) 

有鉴于此,我们可以理解这一切都是关于container_of的力量。还要牢记list_for_each_entry_safe(pos, n, head, member)

内核休眠机制

睡眠是一个进程放松处理器的机制,有可能处理另一个进程。处理器能够休眠的原因可能是为了感知数据可用性,或者等待资源空闲。

内核调度程序管理要运行的任务列表,称为运行队列。休眠进程不再被调度,因为它们被从运行队列中移除。除非它的状态改变(即它醒来),否则永远不会执行睡眠进程。当一个处理器在等待某个东西(资源或其他东西)时,你可以放松它,并确保某个条件或其他人会唤醒它。也就是说,Linux 内核通过提供一组函数和数据结构来简化睡眠机制的实现。

等待队列

等待队列本质上用于处理阻塞的输入/输出,等待特定条件为真,并检测数据或资源可用性。为了了解它是如何工作的,让我们来看看它在include/linux/wait.h中的结构:

struct __wait_queue { 
    unsigned int flags; 
#define WQ_FLAG_EXCLUSIVE 0x01 
    void *private; 
    wait_queue_func_t func; 
    struct list_head task_list; 
}; 

让我们关注task_list场。如你所见,这是一份清单。您想要进入睡眠状态的每个进程都在该列表中排队(因此得名等待队列,并进入睡眠状态,直到某个条件变为真。等待队列只能被看作是一个简单的进程列表和一个锁。

处理等待队列时,您将始终面临以下功能:

  • 静态声明:
DECLARE_WAIT_QUEUE_HEAD(name) 
  • 动态声明:
wait_queue_head_t my_wait_queue; 
init_waitqueue_head(&my_wait_queue); 
  • 阻塞:
/* 
 * block the current task (process) in the wait queue if 
 * CONDITION is false 
 */ 
int wait_event_interruptible(wait_queue_head_t q, CONDITION); 
  • 解除封锁:
/* 
 * wake up one process sleeping in the wait queue if  
 * CONDITION above has become true 
 */ 
void wake_up_interruptible(wait_queue_head_t *q); 

wait_event_interruptible不连续轮询,只是在调用时评估条件。如果条件为假,进程将进入TASK_INTERRUPTIBLE状态,并从运行队列中删除。只有当你每次在等待队列中呼叫wake_up_interruptible时,情况才会被再次检查。如果在wake_up_interruptible运行时条件为真,等待队列中的一个进程将被唤醒,其状态设置为TASK_RUNNING。进程按照它们被置于睡眠状态的顺序被唤醒。要唤醒队列中等待的所有进程,您应该使用wake_up_interruptible_all

In fact, the main functions are wait_event, wake_up, and wake_up_all. They are used with processes in the queue in an exclusive (uninterruptible) wait, since they can't be interrupted by the signal. They should be used only for critical tasks. Interruptible functions are just optional (but recommended). Since they can be interrupted by signals, you should check their return value. A nonzero value means your sleep has been interrupted by some sort of signal, and the driver should return ERESTARTSYS.

如果有人打了wake_upwake_up_interruptible,条件还是FALSE,那就什么都不会发生。没有wake_up(或wake_up_interuptible,进程永远不会被唤醒。下面是一个等待队列的例子:

#include <linux/module.h> 
#include <linux/init.h> 
#include <linux/sched.h> 
#include <linux/time.h> 
#include <linux/delay.h> 
#include<linux/workqueue.h> 

static DECLARE_WAIT_QUEUE_HEAD(my_wq); 
static int condition = 0; 

/* declare a work queue*/        
static struct work_struct wrk; 

static void work_handler(struct work_struct *work) 
{  
    printk("Waitqueue module handler %s\n", __FUNCTION__); 
    msleep(5000); 
    printk("Wake up the sleeping module\n"); 
    condition = 1; 
    wake_up_interruptible(&my_wq); 
} 

static int __init my_init(void) 
{ 
    printk("Wait queue example\n"); 

    INIT_WORK(&wrk, work_handler); 
    schedule_work(&wrk); 

    printk("Going to sleep %s\n", __FUNCTION__); 
    wait_event_interruptible(my_wq, condition != 0); 

    pr_info("woken up by the work job\n"); 
    return 0; 
} 

void my_exit(void) 
{ 
    printk("waitqueue example cleanup\n"); 
} 

module_init(my_init); 
module_exit(my_exit); 
MODULE_AUTHOR("John Madieu <[email protected]>"); 
MODULE_LICENSE("GPL"); 

在上例中,当前进程(实际上是insmod)将在等待队列中休眠 5 秒钟,并被工作处理程序唤醒。dmesg输出如下:

    [342081.385491] Wait queue example
    [342081.385505] Going to sleep my_init
    [342081.385515] Waitqueue module handler work_handler
    [342086.387017] Wake up the sleeping module
    [342086.387096] woken up by the work job
    [342092.912033] waitqueue example cleanup

延迟和定时器管理

时间是最常用的资源之一,仅次于记忆。它被用来做几乎所有的事情:推迟工作、睡眠、日程安排、超时和许多其他任务。

有两类时间。内核使用绝对时间来知道现在是什么时间,也就是一天中的日期和时间,而相对时间由例如内核调度器使用。对于绝对时间,有一个硬件芯片叫做实时时钟 ( RTC )。我们将在本书后面的第 18 章RTC 驱动中讨论这些设备。另一方面,为了处理相对时间,内核依赖于一个 CPU 特性(外设),称为定时器,从内核的角度来看,称为内核定时器。内核定时器是我们将在本节中讨论的内容。

内核定时器分为两个不同的部分:

  • 标准计时器或系统计时器
  • 高分辨率计时器

标准计时器

标准定时器是在 jiffies 粒度上运行的内核定时器。

Jiffies 和 HZ

瞬间是在<linux/jiffies.h>中声明的核心时间单位。为了理解 jiffies,我们需要引入一个新的常数 HZ,即jiffies在一秒内递增的次数。每个增量被称为一个刻度。换句话说,HZ 代表瞬间的大小。HZ 取决于硬件和内核版本,也决定了时钟中断触发的频率。这在某些架构上是可配置的,在其他架构上是固定的。

意思是jiffies每秒增加 HZ 次。如果 HZ = 1,000,则递增 1,000 次(即每 1/1,000 秒一次)。定义后,可编程中断定时器 ( PIT )是一个硬件组件,用该值进行编程,以便在 PIT 中断进入时增加 jiffies。

根据平台的不同,jiffies 可能会导致溢出。在 32 位系统上,HZ = 1,000 只会产生大约 50 天的持续时间,而在 64 位系统上,持续时间大约为 6 亿年。通过将 jiffies 存储在 64 位变量中,问题得以解决。第二个变量已经在<linux/jiffies.h>中引入和定义:

extern u64 jiffies_64; 

在 32 位系统上,以这种方式,jiffies将指向低阶 32 位,jiffies_64将指向高阶位。在 64 位平台上,jiffies = jiffies_64

计时器应用编程接口

定时器在内核中表示为timer_list的一个实例:

#include <linux/timer.h> 

struct timer_list { 
    struct list_head entry; 
    unsigned long expires; 
    struct tvec_t_base_s *base; 
    void (*function)(unsigned long); 
    unsigned long data; 
); 

expires是一个以吉菲兹为单位的绝对值。entry是双向链表,data是可选的,传递给回调函数。

定时器设置初始化

以下是初始化计时器的步骤:

  1. **设置定时器:**设置定时器,输入自定义回拨和数据:
void setup_timer( struct timer_list *timer, \ 
           void (*function)(unsigned long), \ 
           unsigned long data); 

也可以使用这个:

void init_timer(struct timer_list *timer); 

setup_timerinit_timer的包装。

  1. **设置到期时间:**定时器初始化时,我们需要在回调触发前设置它的到期时间:
int mod_timer( struct timer_list *timer, unsigned long expires); 
  1. **释放定时器:**当你用完定时器后,需要释放它:
void del_timer(struct timer_list *timer); 
int del_timer_sync(struct timer_list *timer); 

del_timer返回void是否关闭了等待定时器。它的返回值在非活动计时器上是0,或者在活动计时器上是1。最后一个,del_timer_sync,等待处理程序完成它的执行,甚至那些可能发生在另一个中央处理器上的。您不应该持有阻止处理程序完成的锁,否则将导致死锁。您应该在模块清理例程中释放计时器。您可以独立检查计时器是否在运行:

int timer_pending( const struct timer_list *timer); 

此函数检查是否有任何触发的计时器回调挂起。

标准计时器示例

#include <linux/init.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/timer.h> 

static struct timer_list my_timer; 

void my_timer_callback(unsigned long data) 
{ 
    printk("%s called (%ld).\n", __FUNCTION__, jiffies); 
} 

static int __init my_init(void) 
{ 
    int retval; 
    printk("Timer module loaded\n"); 

    setup_timer(&my_timer, my_timer_callback, 0); 
    printk("Setup timer to fire in 300ms (%ld)\n", jiffies); 

    retval = mod_timer( &my_timer, jiffies + msecs_to_jiffies(300) ); 
    if (retval) 
        printk("Timer firing failed\n"); 

    return 0; 
} 

static void my_exit(void) 
{ 
    int retval; 
    retval = del_timer(&my_timer); 
    /* Is timer still active (1) or no (0) */ 
    if (retval) 
        printk("The timer is still in use...\n"); 

    pr_info("Timer module unloaded\n"); 
} 

module_init(my_init); 
module_exit(my_exit); 
MODULE_AUTHOR("John Madieu <[email protected]>"); 
MODULE_DESCRIPTION("Standard timer example"); 
MODULE_LICENSE("GPL"); 

高分辨率计时器

标准计时器不太精确,不适合实时应用。内核 v2.6.16 中引入的高分辨率计时器(由内核配置中的CONFIG_HIGH_RES_TIMERS选项启用)的分辨率为微秒(最高可达纳秒,具体取决于平台),而标准计时器的分辨率为毫秒。标准定时器依赖于 HZ(因为它们依赖于 jiffies),而 HRT 实现基于ktime

在系统上使用之前,内核和硬件必须支持 HRT。换句话说,必须实现一个依赖于 arch 的代码来访问您的硬件 HRTs。

HR 的 API

所需的标题是:

#include <linux/hrtimer.h> 

HRT 在内核中表示为hrtimer的一个实例:

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; 
}; 

心率变异性设置初始化

  1. 初始化 hrtimer :在 hrtimer 初始化之前,需要设置一个ktime,代表时长。我们将在下面的示例中看到如何实现这一点:
 void hrtimer_init( struct hrtimer *time, clockid_t which_clock, 
                    enum hrtimer_mode mode); 
  1. 启动 hrtimer :如下例所示可以启动 hrtimer:
int hrtimer_start( struct hrtimer *timer, ktime_t time, 
                    const enum hrtimer_mode mode); 

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

  1. HR 定时器取消:你可以取消定时器,也可以看看是否可以取消:
int hrtimer_cancel( struct hrtimer *timer); 
int hrtimer_try_to_cancel(struct hrtimer *timer); 

当定时器未激活时,两者都返回0,当定时器激活时,两者都返回1。这两个功能的区别在于hrtimer_try_to_cancel在定时器激活或者回调运行时失败,返回-1,而hrtimer_cancel将等待回调结束。

我们可以通过以下方式独立检查 hrtimer 的回调是否仍在运行:

int hrtimer_callback_running(struct hrtimer *timer); 

记住,hrtimer_try_to_cancel内部调用hrtimer_callback_running

In order to prevent the timer from automatically restarting, the hrtimer callback function must return HRTIMER_NORESTART.

您可以通过执行以下操作来检查系统上是否有人力资源终端:

  • 通过查看内核配置文件,它应该包含类似CONFIG_HIGH_RES_TIMERS=y: zcat /proc/configs.gz | grep CONFIG_HIGH_RES_TIMERS的内容。
  • 通过查看cat /proc/timer_listcat /proc/timer_list | grep resolution结果。.resolution条目必须显示 1 纳秒,事件处理程序必须显示hrtimer_interrupts
  • 通过使用clock_getres系统调用。
  • 从内核代码中,通过使用#ifdef CONFIG_HIGH_RES_TIMERS

在您的系统上启用了 HRTs 后,睡眠和定时器系统调用的准确性不再依赖于 jiffies,但它们仍然像 HRTs 一样准确。这就是有些系统不支持nanosleep()的原因,比如。

动态滴答/发痒内核

使用以前的 HZ 选项,内核每秒中断 HZ 次,以便重新安排任务,即使在空闲状态下也是如此。如果将 HZ 设置为 1000,每秒会有 1000 个内核中断,防止 CPU 长时间闲置,从而影响 CPU 功耗。

现在让我们看看一个没有固定或预定义记号的内核,其中记号被禁用,直到需要执行某个任务。我们称这样的内核为挠痒痒内核。事实上,滴答激活是根据下一个动作来安排的。正确的名字应该是动态勾核。内核负责任务调度,并维护系统中可运行任务的列表(运行队列)。当没有要调度的任务时,调度器切换到空闲线程,这通过禁用周期性滴答直到下一个定时器到期(新任务排队等待处理)来启用动态滴答。

在引擎盖下,内核还维护一个任务超时列表(然后它知道什么时候和多长时间它必须休眠)。在空闲状态下,如果下一个刻度比任务列表超时中的最低超时更远,内核将使用该超时值对计时器进行编程。当定时器到期时,内核重新启用周期性的滴答并调用调度器,然后调度器调度与超时相关的任务。这就是备忘录内核如何消除周期性的勾号,并在空闲时省电。

内核中的延迟和睡眠

在不深入细节的情况下,有两种类型的延迟,这取决于代码运行的环境:原子的或非原子的。处理内核延迟的强制头是#include <linux/delay>.

原子上下文

原子上下文中的任务(如 ISR)无法休眠,也无法调度;这就是为什么在原子上下文中使用忙等待循环来延迟的原因。内核公开了Xdelay系列函数,这些函数将在一个繁忙的循环中花费足够长的时间(基于 jiffies)来实现期望的延迟:

  • ndelay(unsigned long nsecs)
  • udelay(unsigned long usecs)
  • mdelay(unsigned long msecs)

您应该始终使用udelay(),因为ndelay()的精度取决于您的硬件定时器的精度(在嵌入式 SOC 上并不总是如此)。也不鼓励使用mdelay()

计时器处理程序(回调)在原子上下文中执行,这意味着根本不允许休眠。我所说的睡眠,是指任何可能导致调用者进入睡眠状态的函数,比如分配内存、锁定互斥体、对sleep()函数的显式调用等等。

非原子上下文

在非原子环境中,内核提供了sleep[_range]函数族,使用哪个函数取决于您需要延迟多长时间:

  • udelay(unsigned long usecs):基于繁忙等待循环。如果需要睡眠几秒钟(< ~10 us),应该使用此功能。
  • usleep_range(unsigned long min, unsigned long max):依赖 hrtimers,建议让这个休眠几~秒或者小毫秒(10 us - 20 ms),避免udelay()的忙-等循环。
  • msleep(unsigned long msecs):由 jiffies/legacy_timers 支持。您应该将此用于更大的 msecs 睡眠(10 ms+)。

Sleep and delay topics are well explained in Documentation/timers/timers-howto.txt in the kernel source.

内核锁定机制

锁定是一种有助于在不同线程或进程之间共享资源的机制。共享资源是至少两个用户可以同时或不同时访问的数据或设备。锁定机制防止滥用访问,例如,一个进程在另一个进程在同一位置读取数据时写入数据,或者两个进程访问同一设备(例如,同一 GPIO)。内核提供了几种锁定机制。最重要的是:

  • 互斥(体)…
  • 旗语
  • 斯宾洛克

我们将只了解互斥体和自旋锁,因为它们广泛用于设备驱动。

互斥(体)…

互斥 ( 互斥)是事实上使用最多的锁定机制。为了理解它是如何工作的,让我们看看它的结构在include/linux/mutex.h中是什么样子的:

struct mutex { 
    /* 1: unlocked, 0: locked, negative: locked, possible waiters */ 
    atomic_t count; 
    spinlock_t wait_lock; 
    struct list_head wait_list; 
    [...] 
}; 

正如我们在等待队列部分看到的,结构中还有一个list类型字段:wait_list。睡觉的原理是一样的。

竞争者被从调度器运行队列中移除,并被放入处于睡眠状态的等待列表(wait_list)中。然后内核调度并执行其他任务。当锁被释放时,等待队列中的服务员被唤醒,移出wait_list,并被安排返回。

互斥 API

使用互斥只需要几个基本功能:

声明

  • 静态:
DEFINE_MUTEX(my_mutex); 
  • 动态地:
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); 

有时,您可能只需要检查互斥体是否被锁定。为此,您可以使用int mutex_is_locked(struct mutex *lock)功能。

int mutex_is_locked(struct mutex *lock); 

这个函数所做的只是检查互斥体的所有者是否为空(NULL)。还有mutex_trylock,如果还没有锁定就获取互斥,返回1;否则,返回0:

int mutex_trylock(struct mutex *lock); 

与等待队列的可中断系列功能一样,推荐的mutex_lock_interruptible()将导致驱动能够被任何信号中断,而对于mutex_lock_killable(),只有终止进程的信号才能中断驱动。

使用mutex_lock()要非常小心,在可以保证互斥量会被释放的时候使用,不管发生什么。在用户上下文中,建议您总是使用mutex_lock_interruptible()来获取互斥体,因为如果收到信号mutex_lock()将不会返回(即使是 c trl + c )。

下面是一个互斥实现的例子:

struct mutex my_mutex; 
mutex_init(&my_mutex); 

/* inside a work or a thread */ 
mutex_lock(&my_mutex); 
access_shared_memory(); 
mutex_unlock(&my_mutex); 

请看一下内核源码中的include/linux/mutex.h,看看互斥体必须遵守的严格规则。以下是其中的一些:

  • 一次只能有一个任务持有互斥体;这实际上不是规则,而是事实
  • 不允许多重解锁
  • 它们必须通过应用编程接口进行初始化
  • 持有互斥锁的任务可能不会退出,因为互斥锁将保持锁定,可能的竞争者将永远等待(休眠)
  • 不得释放持有锁所在的内存区域
  • 不得重新初始化保留的互斥体
  • 由于它们涉及重新调度,互斥体可能不会在原子上下文中使用,例如小任务和定时器

As with wait_queue, there is no polling mechanism with mutexes. Every time that mutex_unlock is called on a mutex, the kernel checks for waiters in wait_list. If any, one (and only one) of them is awakened and scheduled; they are woken in the same order in which they were put to sleep.

斯宾洛克

像互斥一样,自旋锁是一种互斥机制;它只有两种状态:

  • 锁定(紧急)
  • 解锁(释放)

任何需要获取自旋锁的线程都将激活循环,直到获取锁,从而脱离循环。这就是互斥体和自旋锁不同的地方。由于自旋锁在循环时会大量消耗 CPU,因此应该将其用于非常快速的获取,尤其是当持有自旋锁的时间少于重新调度的时间时。关键任务完成后,应该立即释放自旋锁。

为了避免通过调度一个可能旋转的线程来浪费 CPU 时间,尝试获取由从运行队列中移出的另一个线程持有的锁,只要持有旋转锁的代码正在运行,内核就禁用抢占。在禁用抢占的情况下,我们防止自旋锁持有人被移出运行队列,这可能导致等待进程长时间旋转并消耗 CPU。

只要一个人持有自旋锁,其他任务就可能在等待它的时候旋转。通过使用 spinlock,您可以断言并保证它不会被长期持有。你可以说,在一个循环中旋转,浪费 CPU 时间,比睡眠你的线程,上下文转移到另一个线程或进程,然后被唤醒的成本更好。在处理器上旋转意味着没有其他任务可以在该处理器上运行;那么在单核机器上使用 spinlock 就没有意义了。在最好的情况下,你会让系统变慢;在最坏的情况下,你会死锁,就像互斥一样。由于这个原因,内核只是在单处理器上响应spin_lock(spinlock_t *lock)功能禁用抢占。在单处理器(核心)系统上,应该使用spin_lock_irqsave()spin_unlock_irqrestore(),这两个选项将分别禁用 CPU 上的中断,防止中断并发。

由于您事先不知道您将为哪个系统编写驱动,建议您使用spin_lock_irqsave(spinlock_t *lock, unsigned long flags)获取一个自旋锁,在获取自旋锁之前禁用当前处理器(调用它的处理器)上的中断。spin_lock_irqsave内部调用local_irq_save(flags);,这是一个依赖于架构的函数,用于保存 IRQ 状态,而preempt_disable()用于禁用相关 CPU 上的抢占。然后,您应该使用spin_unlock_irqrestore()释放锁,这与我们之前列举的操作相反。这是一个可以锁定获取和释放的代码。它是一个 IRQ 处理程序,但是让我们只关注锁方面。我们将在下一节讨论更多关于 IRQ 处理程序的内容:

/* some where */ 
spinlock_t my_spinlock; 
spin_lock_init(my_spinlock); 

static irqreturn_t my_irq_handler(int irq, void *data) 
{ 
    unsigned long status, flags; 

    spin_lock_irqsave(&my_spinlock, flags); 
    status = access_shared_resources(); 

    spin_unlock_irqrestore(&gpio->slock, flags); 
    return IRQ_HANDLED; 
} 

自旋锁与互斥锁

用于内核中的并发,自旋锁和互斥锁都有自己的目标:

  • 互斥体保护进程的关键资源,而自旋锁保护 IRQ 处理程序的关键部分
  • 互斥锁让竞争者休眠,直到获得锁,而自旋锁在循环中无限旋转(消耗 CPU),直到获得锁
  • 由于前一点,您不能长时间持有自旋锁,因为等待者会浪费 CPU 时间来等待锁,而互斥锁只要资源需要保护就可以持有,因为竞争者在等待队列中处于休眠状态

When dealing with spinlocks, please keep in mind that preemption is disabled only for threads holding spinlocks, not for spinning waiters.

工作延期机制

延期是一种方法,通过它你可以安排一项工作在将来执行。这是一种稍后报告行动的方式。显然,内核提供了实现这种机制的工具;它允许您推迟函数的调用和执行,不管它们是什么类型。内核中有三个:

  • 软指令:在原子上下文中执行
  • 小任务:在原子上下文中执行
  • 工作队列:在流程上下文中执行

Softirqs 和 ksoftirqd

软件 IRQ ( 软件 irq )或软件中断是一种延迟机制,仅用于非常快速的处理,因为它在禁用的调度程序下运行(在中断上下文中)。你很少(几乎永远不会)想直接和 softirq 打交道。只有网络和块设备子系统使用 softirq。小任务是 soft IRQ 的一个实例,在你觉得需要使用 soft IRQ 的几乎所有情况下都足够了。

ksoftirqd(德国)

在大多数情况下,软 IRQ 是在硬件中断中调度的,这可能会非常快地到达,比它们能够被服务的速度更快。然后它们被内核排队,以便以后处理。 Ksoftirqds 负责延迟执行(这次是流程上下文)。ksoftirqd 是为处理未服务的软件中断而引发的每 CPU 内核线程:

在我的个人电脑上的上述top示例中,您可以看到ksoftirqd/n条目,其中n是 ksoftirqd 运行的 CPU 号。消耗 CPU 的 ksoftirqd 可能表示系统过载或中断风暴下的系统,这从来都不是好事。你可以看看kernel/softirq.c看看 ksoftirqds 是怎么设计的。

小任务

小任务是建立在 softirqs 之上的下半部分(我们将在后面看到这意味着什么)机制。它们在内核中被表示为结构tasklet_struct的实例:

struct tasklet_struct 
{ 
    struct tasklet_struct *next; 
    unsigned long state; 
    atomic_t count; 
    void (*func)(unsigned long); 
    unsigned long data; 
}; 

小任务本质上是不可重入的。如果一个代码在执行过程中可以在任何地方被中断,然后被安全地再次调用,那么这个代码就叫做可重入代码。小任务的设计使得一个小任务可以同时在一个且只有一个 CPU 上运行(甚至在 SMP 系统上),该 CPU 是它被调度的 CPU,但是不同的小任务可以同时在不同的 CPU 上运行。小任务应用编程接口非常基本和直观。

声明小任务

  • 动态地:
void tasklet_init(struct tasklet_struct *t, 
          void (*func)(unsigned long), unsigned long data); 
  • 静态:
DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data ); 
DECLARE_TASKLET_DISABLED(name, func, data); 

这两种功能有一个区别;前者通过将count字段设置为0来创建一个已经启用并准备好进行调度的小任务,而后者通过将count设置为1来创建一个被禁用的小任务,在小任务可调度之前,必须在其上调用tasklet_enable():

#define DECLARE_TASKLET(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } 

#define DECLARE_TASKLET_DISABLED(name, func, data) \ 
    struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1),  func, data } 

在全局范围内,将count字段设置为0意味着小任务被禁用且无法执行,而非零值则意味着相反。

启用和禁用小任务

启用小任务有一个功能:

void tasklet_enable(struct tasklet_struct *); 

tasklet_enable简单启用小任务。在旧的内核版本中,您可能会发现使用了 void tasklet_hi_enable(struct tasklet_struct *),但是这两个函数做的完全一样。要禁用小任务,请调用:

void tasklet_disable(struct tasklet_struct *); 

您也可以拨打:

void tasklet_disable_nosync(struct tasklet_struct *); 

tasklet_disable将禁用小任务,仅当小任务终止执行时(如果它正在运行)才返回,而tasklet_disable_nosync会立即返回,即使终止没有发生。

小任务调度

根据小任务的优先级是正常还是更高,小任务有两种调度功能:

void tasklet_schedule(struct tasklet_struct *t); 
void tasklet_hi_schedule(struct tasklet_struct *t);

内核在两个不同的列表中维护正常优先级和高优先级的小任务。tasklet_schedule将小任务添加到正常优先级列表中,用TASKLET_SOFTIRQ标志调度相关的软任务。通过tasklet_hi_schedule,小任务被添加到高优先级列表中,用HI_SOFTIRQ标志调度相关的软任务。高优先级小任务旨在用于低延迟要求的软中断处理程序。您应该知道与小任务相关的一些属性:

  • 对已经调度但尚未开始执行的小任务调用tasklet_schedule不会有任何作用,导致小任务只执行一次。
  • tasklet_schedule可以在小任务中调用,意味着小任务可以自己重新调度。
  • 高优先级的小任务总是在正常任务之前执行。滥用高优先级任务会增加系统延迟。只用于真正快速的事情。

您可以使用tasklet_kill功能停止小任务,该功能将阻止小任务再次运行,或者如果小任务当前计划运行,请等待其完成后再将其终止:

void tasklet_kill(struct tasklet_struct *t); 

让我们检查一下。请看下面的例子:

#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/interrupt.h>    /* for tasklets API */ 

char tasklet_data[]="We use a string; but it could be pointer to a structure"; 

/* Tasklet handler, that just print the data */ 
void tasklet_work(unsigned long data) 
{ 
    printk("%s\n", (char *)data); 
} 

DECLARE_TASKLET(my_tasklet, tasklet_function, (unsigned long) tasklet_data); 

static int __init my_init(void) 
{ 
    /* 
     * Schedule the handler. 
     * Tasklet arealso scheduled from interrupt handler 
     */ 
    tasklet_schedule(&my_tasklet); 
    return 0; 
} 

void my_exit(void) 
{ 
    tasklet_kill(&my_tasklet); 
} 

module_init(my_init); 
module_exit(my_exit); 
MODULE_AUTHOR("John Madieu <[email protected]>"); 
MODULE_LICENSE("GPL"); 

工作队列

自从 Linux 内核 2.6 以来,最常用和最简单的延迟机制是工作队列。这是我们将在本章中讨论的最后一个问题。作为一种延迟机制,它采取了与我们所看到的其他机制相反的方法,只在可抢占的上下文中运行。当你需要睡在下半身时,这是唯一的选择(我将在下一节稍后解释什么是下半身)。我所说的睡眠是指处理输入/输出数据、保持互斥体、延迟以及所有其他可能导致睡眠或将任务移出运行队列的任务。

请记住,工作队列是建立在内核线程之上的,这就是我决定不把内核线程作为一种延迟机制的原因。然而,在内核中有两种方法来处理工作队列。首先,有一个默认的共享工作队列,由一组内核线程处理,每个线程运行在一个中央处理器上。一旦有了要调度的工作,就将该工作排队到全局工作队列中,该队列将在适当的时候执行。另一种方法是在专用内核线程中运行工作队列。这意味着每当需要执行您的工作队列处理程序时,您的内核线程都会被唤醒来处理它,而不是默认的预定义线程。

要调用的结构和函数是不同的,这取决于您选择的是共享工作队列还是专用工作队列。

内核-全局工作队列-共享队列

除非你别无选择,或者你需要关键的性能,或者你需要控制从工作队列初始化到工作调度的一切,如果你只是偶尔提交任务,你应该使用内核提供的共享工作队列。由于该队列在系统上共享,您应该很友好,不应该长时间独占该队列。

由于队列中挂起任务的执行是在每个 CPU 上序列化的,所以您不应该长时间睡眠,因为在您醒来之前,队列中没有其他任务会运行。你甚至不知道你和谁共享工作队列,所以如果你的任务需要更长的时间来获得 CPU,不要感到惊讶。共享工作队列中的工作在内核创建的名为 events/n 的每 CPU 线程中执行。

在这种情况下,工作也必须用INIT_WORK宏初始化。因为我们将使用共享工作队列,所以不需要创建工作队列结构。我们只需要将作为参数传递的work_struct结构。有三种功能可以调度共享工作队列上的工作:

  • 将工作与当前中央处理器相关联的版本:
int schedule_work(struct work_struct *work); 
  • 相同但延迟的功能:
static inline bool schedule_delayed_work(struct delayed_work *dwork, 
                            unsigned long delay) 
  • 实际调度给定 CPU 上的工作的函数:
int schedule_work_on(int cpu, struct work_struct *work); 
  • 与前面显示的相同,但有一个延迟:
int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay); 

所有这些函数都将作为参数给出的工作调度到系统的共享工作队列system_wq,在kernel/workqueue.c中定义:

struct workqueue_struct *system_wq __read_mostly; 
EXPORT_SYMBOL(system_wq); 

已经提交到共享队列的工作可以通过cancel_delayed_work功能取消。您可以使用以下命令刷新共享工作队列:

void flush_scheduled_work(void); 

由于队列是在系统上共享的,人们无法真正知道flush_scheduled_work()在返回之前会持续多长时间:

#include <linux/module.h> 
#include <linux/init.h> 
#include <linux/sched.h>    /* for sleep */ 
#include <linux/wait.h>     /* for wait queue */ 
#include <linux/time.h> 
#include <linux/delay.h> 
#include <linux/slab.h>         /* for kmalloc() */ 
#include <linux/workqueue.h> 

//static DECLARE_WAIT_QUEUE_HEAD(my_wq); 
static int sleep = 0; 

struct work_data { 
    struct work_struct my_work; 
    wait_queue_head_t my_wq; 
    int the_data; 
}; 

static void work_handler(struct work_struct *work) 
{ 
    struct work_data *my_data = container_of(work, \ 
                                 struct work_data, my_work);  
    printk("Work queue module handler: %s, data is %d\n", __FUNCTION__, my_data->the_data); 
    msleep(2000); 
    wake_up_interruptible(&my_data->my_wq); 
    kfree(my_data); 
} 

static int __init my_init(void) 
{ 
    struct work_data * my_data; 

    my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL); 
    my_data->the_data = 34; 

    INIT_WORK(&my_data->my_work, work_handler); 
    init_waitqueue_head(&my_data->my_wq); 

    schedule_work(&my_data->my_work); 
    printk("I'm goint to sleep ...\n"); 
    wait_event_interruptible(my_data->my_wq, sleep != 0); 
    printk("I am Waked up...\n"); 
    return 0; 
} 

static void __exit my_exit(void) 
{ 
    printk("Work queue module exit: %s %d\n", __FUNCTION__,  __LINE__); 
} 

module_init(my_init); 
module_exit(my_exit); 
MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("John Madieu <[email protected]> "); 
MODULE_DESCRIPTION("Shared workqueue"); 

In order to pass data to my work queue handler, you may have noticed that in both examples, I've embedded my work_struct structure inside my custom data structure, and used container_of to retrieve it. It is the common way to pass data to the work queue handler.

专用工作队列

这里,工作队列被表示为struct workqueue_struct的一个实例。要排入工作队列的工作被表示为struct work_struct的一个实例。在自己的内核线程中安排工作之前,有四个步骤:

  1. 声明/初始化一个struct workqueue_struct
  2. 创建您的工作函数。
  3. 创建一个struct work_struct,这样你的功函数就会嵌入其中。
  4. 将您的工作功能嵌入到work_struct中。

编程语法

include/linux/workqueue.h中定义了以下功能:

  • 申报工作和工作队列:
struct workqueue_struct *myqueue; 
struct work_struct thework; 
  • 定义工作函数(处理程序):
void dowork(void *data) {  /* Code goes here */ }; 
  • 初始化我们的工作队列,并将我们的工作嵌入到:
myqueue = create_singlethread_workqueue( "mywork" ); 
INIT_WORK( &thework, dowork, <data-pointer> ); 

我们也可以通过一个名为create_workqueue的宏来创建我们的工作队列。create_workqueuecreate_singlethread_workqueue的区别在于前者将创建一个工作队列,而后者将在每个可用的处理器上创建一个独立的内核线程。

  • 计划工作:
queue_work(myqueue, &thework); 

给定工作线程的给定延迟后排队:

    queue_dalayed_work(myqueue, &thework, <delay>); 

如果工作已经在队列中,这些函数返回false,否则返回truedelay表示排队前等待的秒数。您可以使用辅助功能msecs_to_jiffies将标准毫秒延迟转换为吉菲兹。例如,要在 5 毫秒后排队工作,可以使用queue_delayed_work(myqueue, &thework, msecs_to_jiffies(5));

  • 等待给定工作队列中的所有挂起工作:
void flush_workqueue(struct workqueue_struct *wq) 

flush_workqueue休眠,直到所有排队的工作完成执行。新的传入(排队)工作不会影响睡眠。通常可以在驱动关闭处理程序中使用它。

  • 清理:

使用cancel_work_sync()cancel_delayed_work_sync进行同步取消,如果还没有运行的话会取消工作,或者一直阻塞到工作完成。这项工作即使重新要求也将被取消。您还必须确保在处理程序返回之前,工作最后排队的工作队列不会被破坏。这些功能将分别用于未显示或延迟的工作:

int cancel_work_sync(struct work_struct *work); 
int cancel_delayed_work_sync(struct delayed_work *dwork); 

从 Linux 内核 v4.8 开始,可以使用cancel_workcancel_delayed_work,这是取消的异步形式。必须检查函数是否返回 true 或 no,并确保工作本身不会重新查询。然后,您必须显式刷新工作队列:

if ( !cancel_delayed_work( &thework) ){
flush_workqueue(myqueue);
destroy_workqueue(myqueue);
}

另一个是同一方法的不同版本,将只为所有处理器创建一个线程。如果在工作入队之前需要延迟,可以使用以下工作初始化宏:

INIT_DELAYED_WORK(_work, _func); 
INIT_DELAYED_WORK_DEFERRABLE(_work, _func); 

使用前面的宏意味着您应该使用以下函数来对工作队列中的工作进行排队或调度:

int queue_delayed_work(struct workqueue_struct *wq, 
            struct delayed_work *dwork, unsigned long delay) 

queue_work将工作绑定到当前的 CPU。您可以使用queue_work_on功能指定处理器运行的中央处理器:

int queue_work_on(int cpu, struct workqueue_struct *wq, 
                   struct work_struct *work); 

对于延迟工作,您可以使用:

int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, 
    struct delayed_work *dwork, unsigned long delay);

以下是使用专用工作队列的示例:

#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/workqueue.h>    /* for work queue */ 
#include <linux/slab.h>         /* for kmalloc() */ 

struct workqueue_struct *wq; 

struct work_data { 
    struct work_struct my_work; 
    int the_data; 
}; 

static void work_handler(struct work_struct *work) 
{ 
    struct work_data * my_data = container_of(work, 
                                   struct work_data, my_work); 
    printk("Work queue module handler: %s, data is %d\n", 
         __FUNCTION__, my_data->the_data); 
    kfree(my_data); 
} 

static int __init my_init(void) 
{ 
    struct work_data * my_data; 

    printk("Work queue module init: %s %d\n", 
           __FUNCTION__, __LINE__); 
    wq = create_singlethread_workqueue("my_single_thread"); 
    my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL); 

    my_data->the_data = 34; 
    INIT_WORK(&my_data->my_work, work_handler); 
    queue_work(wq, &my_data->my_work); 

    return 0; 
} 

static void __exit my_exit(void) 
{ 
    flush_workqueue(wq); 
    destroy_workqueue(wq); 
    printk("Work queue module exit: %s %d\n", 
                   __FUNCTION__, __LINE__); 
} 

module_init(my_init); 
module_exit(my_exit); 
MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("John Madieu <[email protected]>"); 

预定义(共享)工作队列和标准工作队列功能

预定义的工作队列在kernel/workqueue.c中定义如下:

struct workqueue_struct *system_wq __read_mostly; 

它只不过是一个标准的工作,内核为它提供了一个定制的 API,简单地包装了标准的 API。

内核预定义工作队列函数和标准工作队列函数之间的比较如下:

| 预定义工作队列功能 | 等效标准工作队列功能 | | schedule_work(w) | queue_work(keventd_wq,w) | | schedule_delayed_work(w,d) | queue_delayed_work(keventd_wq,w,d)(在任何 CPU 上) | | schedule_delayed_work_on(cpu,w,d) | queue_delayed_work(keventd_wq,w,d)(在给定的中央处理器上) | | flush_scheduled_work() | flush_workqueue(keventd_wq) |

内核线程

工作队列运行在内核线程之上。当您使用工作队列时,您已经使用了内核线程。这就是我决定不谈内核线程 API 的原因。

内核中断机制

中断是设备停止内核的方式,告诉它发生了有趣或重要的事情。这些在 Linux 系统上被称为 IRQs。中断提供的主要优势是避免设备轮询。由设备来判断其状态是否有变化;我们无权投票决定。

为了在中断发生时得到通知,您需要注册到该 IRQ,提供一个名为中断处理程序的函数,每次引发中断时都会调用该函数。

注册中断处理程序

当您感兴趣的中断(或中断线路)被触发时,您可以注册一个回调来运行。您可以通过在<linux/interrupt.h>中声明的功能request_irq()来实现:

int request_irq(unsigned int irq, irq_handler_t handler, 
    unsigned long flags, const char *name, void *dev) 

request_irq()可能失败,成功返回0。前面代码的其他元素详述如下:

  • flags:这些应该是<linux/interrupt.h>中定义的掩码的位掩码。使用最多的是:
    • IRQF_TIMER:通知内核该处理程序是由系统定时器中断发起的。
    • IRQF_SHARED:用于可由两个或多个设备共享的中断线路。共享同一线路的每个设备都必须设置此标志。如果省略,则只能为指定的 IRQ 行注册一个处理程序。
    • IRQF_ONESHOT:主要用于螺纹 IRQ。它指示内核在 hardirq 处理程序完成时不要重新启用中断。在线程处理程序运行之前,它将保持禁用状态。
    • 在旧的内核版本中(直到 v2.6.35),有IRQF_DISABLED标志,它要求内核在处理程序运行时禁用所有中断。此标志不再使用。
  • name:这是内核在/proc/interrupts/proc/irqT3 中用来识别你的驱动的。
  • dev:它的主要目标是作为参数传递给处理程序。这对于每个注册的处理程序应该是唯一的,因为它用于识别设备。非共享 IRQ 可以是NULL,共享 IRQ 不可以。使用它的常见方式是提供一个device结构,因为它既独特又可能对处理者有用。也就是说,指向任何每设备数据结构的指针就足够了:
struct my_data { 
   struct input_dev *idev; 
   struct i2c_client *client; 
   char name[64]; 
   char phys[32]; 
 }; 

 static irqreturn_t my_irq_handler(int irq, void *dev_id) 
 { 
    struct my_data *md = dev_id; 
    unsigned char nextstate = read_state(lp); 
    /* Check whether my device raised the irq or no */ 
    [...] 
    return IRQ_HANDLED; 
 } 

 /* some where in the code, in the probe function */ 
 int ret; 
 struct my_data *md; 
 md = kzalloc(sizeof(*md), GFP_KERNEL); 

 ret = request_irq(client->irq, my_irq_handler, 
                    IRQF_TRIGGER_LOW | IRQF_ONESHOT, 
                    DRV_NAME, md); 

 /* far in the release function */ 
 free_irq(client->irq, md); 
  • handler:这是触发中断时会运行的回调函数。中断处理程序的结构类似于:
static irqreturn_t my_irq_handler(int irq, void *dev) 
  • 它包含以下代码元素:
    • irq:IRQ 的数值(与request_irq中使用的相同)。
    • dev:与request_irq中使用的相同。

这两个参数都是由内核给你的处理器的。处理程序只能返回两个值,具体取决于您的设备是否发起了 IRQ:

  • IRQ_NONE:你的设备不是中断的发起者(尤其是在共享的 IRQ 线路上)
  • IRQ_HANDLED:您的设备导致了中断

根据处理的不同,可以使用IRQ_RETVAL(val)宏,如果值不为零,将返回IRQ_HANDLED,否则返回IRQ_NONE

When writing the interrupt handler, you don't have to worry about reentrancy, since the IRQ line serviced is disabled on all processors by the kernel in order to avoid recursive interrupt.

释放先前注册的处理程序的相关函数是:

void free_irq(unsigned int irq, void *dev) 

如果指定的 IRQ 未共享,free_irq不仅会删除处理程序,还会禁用线路。如果是共享的,只有通过dev识别的处理程序(应该和request_irq中使用的一样)被删除,但中断线路仍然保留,只有在最后一个处理程序被删除时才会被禁用。free_irq将阻塞,直到指定 IRQ 的任何执行中断完成。然后,您必须在中断上下文中避免使用request_irqfree_irq

中断处理程序和锁

不言而喻,您处于原子上下文中,必须只使用自旋锁来实现并发。每当有全局数据可由两个用户代码访问时(用户任务;即系统调用)和中断代码,这种共享数据应该由用户代码中的spin_lock_irqsave()保护。让我们看看为什么我们不能只使用spin_lock.一个中断处理程序在用户任务上总是有优先权的,即使那个任务持有一个自旋锁。仅仅禁用 IRQ 是不够的。中断可能发生在另一个中央处理器上。如果更新数据的用户任务被试图访问相同数据的中断处理程序中断,那将是一场灾难。使用spin_lock_irqsave()将禁用本地中央处理器上的所有中断,防止系统调用被任何类型的中断中断:

ssize_t my_read(struct file *filp, char __user *buf, size_t count,  
   loff_t *f_pos) 
{ 
    unsigned long flags; 
    /* some stuff */ 
    [...] 
    unsigned long flags; 
    spin_lock_irqsave(&my_lock, flags); 
    data++; 
    spin_unlock_irqrestore(&my_lock, flags) 
    [...] 
} 

static irqreturn_t my_interrupt_handler(int irq, void *p) 
{ 
    /* 
     * preemption is disabled when running interrupt handler 
     * also, the serviced irq line is disabled until the handler has completed 
     * no need then to disable all other irq. We just use spin_lock and 
     * spin_unlock 
     */ 
    spin_lock(&my_lock); 
    /* process data */ 
    [...] 
    spin_unlock(&my_lock); 
    return IRQ_HANDLED; 
} 

当在不同的中断处理程序之间共享数据时(也就是说,同一驱动管理两个或多个设备,每个设备都有自己的 IRQ 线路),还应该用这些处理程序中的spin_lock_irqsave()来保护该数据,以防止其他 IRQ 被触发并无用地旋转。

下半部的概念

下半部分是将中断处理程序分成两部分的机制。这引入了另一个术语,即上半部分。在讨论它们之前,让我们先谈谈它们的起源,以及它们解决了什么问题。

问题–中断处理程序设计限制

无论中断处理程序是否持有自旋锁,在运行该处理程序的中央处理器上,抢占都是禁用的。在处理程序中浪费的时间越多,分配给其他任务的 CPU 就越少,这可能会大大增加其他中断的延迟,从而增加整个系统的延迟。挑战在于尽快确认引发中断的设备,以保持系统响应。

在 Linux 系统上(实际上是在所有 OS 上,通过硬件设计),任何中断处理程序在运行时,其当前的中断线路在所有处理器上都被禁用,有时您可能需要禁用实际运行该处理程序的 CPU 上的所有中断,但您肯定不想错过中断。为了满足这一需求,引入了两半的概念。

解决方案——下半部分

这个想法包括将处理程序分成两部分:

  • 第一部分称为上半部分或硬 IRQ,它是使用request_irq()注册的函数,最终将根据需要屏蔽/隐藏中断(在当前的中央处理器上,除了正在服务的中断,因为它在运行处理程序之前已经被内核禁用),执行快速和快速的操作(本质上是对时间敏感的任务、读/写硬件寄存器和对该数据的快速处理),调度第二部分和下一部分,然后确认该行。所有被禁用的中断必须在退出下半部分之前重新启用。
  • 第二部分称为下半部分,将处理耗时的东西,并在中断重新启用的情况下运行。这样,你就有机会不错过一个中断。

下半部分使用我们之前看到的工作延迟机制来设计。根据您选择的方式,它可能在(软件)中断上下文或进程上下文中运行。下半部分的机制是:

  • 软中断
  • 小任务
  • 工作队列
  • 线程 IRQ

Softirqs 和小任务在(软件)中断上下文中执行(意味着抢占被禁用),Workqueues 和线程 irqs 在进程(或简称为任务)上下文中执行,可以被抢占,但没有什么能阻止我们更改它们的实时属性以适应您的需求并更改它们的抢占行为(参见CONFIG_PREEMPTCONFIG_PREEMPT_VOLUNTARY.这也会影响整个系统)。下半部分并不总是可能的。但在可能的情况下,这肯定是最好的做法。

作为下半部分的小任务

小任务延迟机制最常用于 DMA、网络和块设备驱动。只需在内核源代码中尝试以下命令:

 grep -rn tasklet_schedule  

现在让我们看看如何在中断处理程序中实现这样的机制:

struct my_data { 
    int my_int_var; 
    struct tasklet_struct the_tasklet; 
    int dma_request; 
}; 

static void my_tasklet_work(unsigned long data) 
{ 
    /* Do what ever you want here */ 
} 

struct my_data *md = init_my_data; 

/* somewhere in the probe or init function */ 
[...] 
   tasklet_init(&md->the_tasklet, my_tasklet_work, 
                 (unsigned long)md); 
[...] 

static irqreturn_t my_irq_handler(int irq, void *dev_id) 
{ 
    struct my_data *md = dev_id; 

    /* Let's schedule our tasklet */ 
    tasklet_schedule(&md.dma_tasklet); 

    return IRQ_HANDLED; 
} 

在前面的示例中,我们的小任务将执行函数my_tasklet_work()

作为下半部分的工作队列

让我们从一个示例开始:

static DECLARE_WAIT_QUEUE_HEAD(my_wq);  /* declare and init the wait queue */ 
static struct work_struct my_work; 

/* some where in the probe function */ 
/* 
 * work queue initialization. "work_handler" is the call back that will be 
 * executed when our work is scheduled. 
 */ 
INIT_WORK(my_work, work_handler); 

static irqreturn_t my_interrupt_handler(int irq, void *dev_id) 
{ 
    uint32_t val; 
    struct my_data = dev_id; 

    val = readl(my_data->reg_base + REG_OFFSET); 
   if (val == 0xFFCD45EE)) { 
       my_data->done = true; 
         wake_up_interruptible(&my_wq); 
   } else { 
         schedule_work(&my_work); 
   } 

   return IRQ_HANDLED; 
}; 

在前面的示例中,我们使用了等待队列或工作队列来唤醒等待我们的可能正在休眠的进程,或者根据寄存器的值来安排工作。我们没有共享的数据或资源,所以没有必要禁用所有其他 IRQ(spin_lock_irq_disable)。

作为下半部分的软 IRQ

正如本章开头所说,我们将不讨论 softirq。无论你觉得哪里需要使用 softirqs,小任务就足够了。不管怎样,让我们谈谈他们的违约。

Softirqs 在软件中断上下文中运行,抢占被禁用,保持 CPU 直到它们完成。Softirq 应该快;否则,它们可能会降低系统速度。当由于任何原因,软 irq 阻止内核调度其他任务时,任何新的输入软 irq 将由运行在进程上下文中的 ksoftirqd 线程处理。

线程 IRQ

线程化 IRQ 的主要目标是将中断禁用所花费的时间降至最低。对于线程化的 IRQ,注册中断处理程序的方式有点简化。你甚至不需要自己安排下半部分。核心为我们做了这些。然后,下半部分在专用内核线程中执行。我们不再使用request_irq(),而是request_threaded_irq():

int request_threaded_irq(unsigned int irq, irq_handler_t handler,\ 
                            irq_handler_t thread_fn, \ 
                            unsigned long irqflags, \ 
                            const char *devname, void *dev_id) 

request_threaded_irq()函数在其参数中接受两个函数:

  • @handler 功能:这个功能和request_irq()注册的功能一样。它代表上半部分的函数,在原子上下文(或硬-IRQ)中运行。如果它能更快地处理中断,让你完全摆脱下半部分,它应该会返回IRQ_HANDLED。但是,如果中断处理需要超过 100 s,如前所述,您应该使用下半部分。在这种情况下,它应该返回IRQ_WAKE_THREAD,这将导致调度必须已经提供的thread_fn功能。
  • @thread_fn 功能:这代表下半部分,就像你在上半部分计划的那样。当硬-IRQ 处理器(处理器功能)功能返回IRQ_WAKE_THREAD时,与该下半部分相关联的 kthread 将被调度,在运行 ktread 时调用thread_fn功能。完成后,thread_fn功能必须返回IRQ_HANDLED。执行后,kthread 将不会被再次重新调度,直到再次触发 IRQ 并且硬 IRQ 返回IRQ_WAKE_THREAD

无论您在哪里使用工作队列来调度下半部分,都可以使用线程化的 IRQ。handlerthread_fn必须定义,以便有一个适当的线程化 IRQ。如果handlerNULLthread_fn != NULL,内核将安装默认的硬-IRQ 处理程序(见下文),这将简单地返回IRQ_WAKE_THREAD来调度下半部分。handler总是在中断上下文中被调用,无论它是由您自己提供的还是由内核默认提供的:

/* 
 * Default primary interrupt handler for threaded interrupts. Is 
 * assigned as primary handler when request_threaded_irq is called 
 * with handler == NULL. Useful for oneshot interrupts. 
 */ 
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id) 
{ 
    return IRQ_WAKE_THREAD; 
} 

request_threaded_irq(unsigned int irq, irq_handler_t handler, 
                         irq_handler_t thread_fn, unsigned long irqflags, 
                         const char *devname, void *dev_id) 
{ 
        [...] 
        if (!handler) { 
                if (!thread_fn) 
                        return -EINVAL; 
                handler = irq_default_primary_handler; 
        } 
        [...] 
} 
EXPORT_SYMBOL(request_threaded_irq); 

对于线程化的 IRQ,处理程序的定义不会改变,但是注册的方式会有一点改变。

request_irq(unsigned int irq, irq_handler_t handler, \ 
            unsigned long flags, const char *name, void *dev) 
{ 
    return request_threaded_irq(irq, handler, NULL, flags, \ 
                                name, dev); 
} 

螺纹下半部

下面的简单摘录演示了如何实现线程化的下半部分机制:

static irqreturn_t pcf8574_kp_irq_handler(int irq, void *dev_id) 
{ 
    struct custom_data *lp = dev_id; 
    unsigned char nextstate = read_state(lp); 

    if (lp->laststate != nextstate) { 
        int key_down = nextstate < ARRAY_SIZE(lp->btncode); 
        unsigned short keycode = key_down ?  
            p->btncode[nextstate] : lp->btncode[lp->laststate]; 

        input_report_key(lp->idev, keycode, key_down); 
        input_sync(lp->idev); 
        lp->laststate = nextstate; 
    } 
    return IRQ_HANDLED; 
} 

static int pcf8574_kp_probe(struct i2c_client *client, \ 
                          const struct i2c_device_id *id) 
{ 
    struct custom_data *lp = init_custom_data(); 
    [...] 
    /* 
     * @handler is NULL and @thread_fn != NULL 
     * the default primary handler is installed, which will  
     * return IRQ_WAKE_THREAD, that will schedule the thread  
     * asociated to the bottom half. the bottom half must then  
     * return IRQ_HANDLED when finished 
     */ 
    ret = request_threaded_irq(client->irq, NULL, \ 
                            pcf8574_kp_irq_handler, \ 
                            IRQF_TRIGGER_LOW | IRQF_ONESHOT, \ 
                            DRV_NAME, lp); 
    if (ret) { 
        dev_err(&client->dev, "IRQ %d is not free\n", \ 
                 client->irq); 
        goto fail_free_device; 
    } 
    ret = input_register_device(idev); 
    [...] 
} 

When an interrupt handler is executed, the serviced IRQ is always disabled on all CPUs, and re-enabled when the hard-IRQ (top-half) finishes. But if for any reason you need the IRQ line not to be re-enabled after the top half, and to remain disabled until the threaded handler has been run, you should request the threaded IRQ with the flag IRQF_ONESHOT enabled (by just doing an OR operation as shown previously). The IRQ line will then be re-enabled after the bottom half has finished.

从内核调用用户空间应用

用户空间应用大部分时间是由其他应用从用户空间内部调用的。在不深入细节的情况下,让我们看一个例子:

#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/workqueue.h>    /* for work queue */ 
#include <linux/kmod.h> 

static struct delayed_work initiate_shutdown_work; 
static void delayed_shutdown( void ) 
{ 
   char *cmd = "/sbin/shutdown"; 
   char *argv[] = { 
         cmd, 
         "-h", 
         "now", 
         NULL, 
   }; 
   char *envp[] = { 
         "HOME=/", 
         "PATH=/sbin:/bin:/usr/sbin:/usr/bin", 
         NULL, 
   }; 

   call_usermodehelper(cmd, argv, envp, 0); 
} 

static int __init my_shutdown_init( void ) 
{ 
    schedule_delayed_work(&delayed_shutdown, msecs_to_jiffies(200)); 
    return 0; 
} 

static void __exit my_shutdown_exit( void ) 
{ 
  return; 
} 

module_init( my_shutdown_init ); 
module_exit( my_shutdown_exit ); 

MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("John Madieu", <[email protected]>); 
MODULE_DESCRIPTION("Simple module that trigger a delayed shut down"); 

在前面的例子中,使用的应用编程接口(call_usermodehelper)是用户模式助手应用编程接口的一部分,所有功能都在kernel/kmod.c中定义。它的使用相当简单;kmod.c只要看看里面就会给你一个想法。您可能想知道这个应用编程接口是为什么而定义的。它由内核使用,例如,用于模块(取消)加载和 cgroups 管理。

摘要

在本章中,我们讨论了启动驱动开发的基本要素,介绍了驱动中经常使用的每种机制。这一章非常重要,因为它讨论了本书其他章节所依赖的主题。下一章,例如,处理字符设备,将使用本章中讨论的一些元素。