Skip to content

Latest commit

 

History

History
708 lines (547 loc) · 32.7 KB

File metadata and controls

708 lines (547 loc) · 32.7 KB

十一、附加信息:内核内部杂项

这里有一些关于动态内存分配和输入/输出内存访问方法的一般信息。

在谈论动态内存分配时,我们应该记住,我们是在内核中用 C 语言编程的,所以记住每个分配的内存块在不再使用时都必须释放出来,这一点非常重要。这一点非常重要,因为在用户空间中,当一个进程结束其执行时,内核(它实际上知道该进程拥有的所有内存块)可以轻松地取回所有进程分配的内存;但是这对内核来说并不成立。事实上,请求内存块的驱动(或其他内核实体)必须确保释放内存块,否则,没有人会请求释放内存块,并且在机器重新启动之前,内存块将会丢失。

关于对 I/O 内存的访问,这是由外围寄存器下面的存储单元组成的区域,我们必须考虑到我们不能使用它们的物理内存地址来访问它们;相反,我们将不得不使用相应的虚拟。事实上,Linux 是一个使用内存管理单元 ( MMU )来虚拟化和保护内存访问的操作系统,因此我们必须将每个外围设备的物理内存区域重新映射到其对应的虚拟内存区域,以便能够读写它们。

这个操作可以通过使用第章代码片段中的内核函数轻松完成,但最重要的是要指出,它必须在尝试任何输入/输出内存访问之前完成,否则将触发分段错误。这可能会终止用户空间中的进程,或者因为设备驱动中的错误而终止内核本身。

动态存储分配

分配内存最直接的方法就是使用kmalloc()函数,为了安全起见,最好使用将分配的内存清零的例程,比如kzalloc()函数。另一方面,如果我们需要为一个数组分配内存,有kmalloc_array()kcalloc()专用功能。

下面是头文件linux/include/linux/slab.h中报告的一些包含内存分配内核函数(和相对内核内存释放函数)的片段:

/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
...
*/
static __always_inline void *kmalloc(size_t size, gfp_t flags);

/**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO);
}

/**
 * kmalloc_array - allocate memory for an array.
 * @n: number of elements.
 * @size: element size.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kmalloc_array(size_t n, size_t size, gfp_t flags);

/**
 * kcalloc - allocate memory for an array. The memory is set to zero.
 * @n: number of elements.
 * @size: element size.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kcalloc(size_t n, size_t size, gfp_t flags)
{
    return kmalloc_array(n, size, flags | __GFP_ZERO);
}

void kfree(const void *);

所有上述函数揭示了用户空间对应函数malloc()和其他内存分配函数之间的两个主要区别:

  1. 可以分配给kmalloc()和好友的最大块大小是有限的。实际的限制取决于硬件和内核配置,但是对于小于页面大小的对象,使用kmalloc()和其他内核助手是一个很好的做法。

The number of bytes that make a page size is stated by defined PAGE_SIZE info kernel sources in the linux/include/asm-generic/page.h file; usually, it's 4096 bytes for 32-bit systems and 8192 bytes for 64-bit systems. It can be explicitly chosen by the user via the usual kernel configuration mechanism. 

  1. 用于动态内存分配的内核函数,如kmalloc()和类似的函数会额外增加一个参数;分配标志用于以多种方式指定kmalloc()的行为,如下面来自内核源代码的linux/include/linux/slab.h文件的片段中所报告的:
/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * for objects smaller than page size in the kernel.
 *
 * The @flags argument may be one of:
 *
 * %GFP_USER - Allocate memory on behalf of user. May sleep.
 *
 * %GFP_KERNEL - Allocate normal kernel ram. May sleep.
 *
 * %GFP_ATOMIC - Allocation will not sleep. May use emergency pools.
 * For example, use this inside interrupt handlers.
 *
 * %GFP_HIGHUSER - Allocate pages from high memory.
 *
 * %GFP_NOIO - Do not do any I/O at all while trying to get memory.
 *
 * %GFP_NOFS - Do not make any fs calls while trying to get memory.
 *
 * %GFP_NOWAIT - Allocation will not sleep.
...

我们可以看到,有很多旗帜;然而,设备驱动开发人员将主要对GFP_KERNELGFP_ATOMIC感兴趣。

很明显,这两个标志的主要区别在于,前者可以分配正常的内核 RAM,并且它可能会休眠,而后者在不允许调用者休眠的情况下也是如此。这是两个函数之间的一个很大的区别,因为它告诉我们当我们处于中断上下文或进程上下文中时,我们必须使用哪个标志。

第 5 章管理中断和并发、中所见,当我们处于中断上下文中时,我们无法休眠(如上面的代码中所报告的),在这些情况下,我们必须通过指定GFP_ATOMIC标志来调用kmalloc()和朋友,而GFP_KERNEL标志可以在其他地方使用,记住它可以导致调用方休眠,然后 CPU 可能会让我们执行其他东西;因此,我们应该避免做以下事情:

spin_lock(...);
ptr = kmalloc(..., GFP_KERNEL);
spin_unlock(...);

事实上,即使我们在进程上下文中执行,在持有自旋锁的同时执行一个休眠的kmalloc()也被认为是邪恶的!所以,在这种情况下,我们无论如何都必须使用GFP_ATOMIC标志。此外,请注意,成功的GFP_ATOMIC分配请求的最大大小往往小于GFP_KERNEL请求,原因与这里明确提到的相同,涉及物理上连续的内存分配,并且内核保持有限的内存池随时可供原子分配使用。

关于上面的第一点,关于可分配内存块的有限大小,对于大的分配,我们可以考虑使用另一类函数:vmalloc()vzalloc(),即使我们必须强调这样一个事实,即由vmalloc()和相关函数分配的内存不是物理上连续的,不能用于直接内存访问 ( DMA )活动(而kmalloc()和 friends,如前所述,在虚拟和物理寻址空间中分配连续的内存区域)。

Allocating memory for DMA activities is currently not addressed in this book; however, you may get further information regarding this issue in kernel sources within the linux/Documentation/DMA-API.txt and linux/Documentation/DMA-API-HOWTO.txt files.

以下是linux/include/linux/vmalloc.h头文件中报告的vmalloc()函数和好友定义的原型:

extern void *vmalloc(unsigned long size);
extern void *vzalloc(unsigned long size);

如果不确定kmalloc()的分配规模是否过大,可以使用kvmalloc()及其派生词。该功能将尝试使用kmalloc()分配内存,如果分配失败,将退回到vmalloc()

Note that kvmalloc()may return memory that is not physically contiguous.

kvmalloc_node()文档中https://www . kernel . org/doc/html/latest/core-API/mm-API . html # c . kvmalloc _ node所述,GFP_*标志也有使用限制。

以下是linux/include/linux/mm.h头文件中报告的关于kvmalloc()kvzalloc()kvmalloc_array()kvcalloc()kvfree()的代码片段:

static inline void *kvmalloc(size_t size, gfp_t flags)
{
    return kvmalloc_node(size, flags, NUMA_NO_NODE);
}

static inline void *kvzalloc(size_t size, gfp_t flags)
{
    return kvmalloc(size, flags | __GFP_ZERO);
}

static inline void *kvmalloc_array(size_t n, size_t size, gfp_t flags)
{
    size_t bytes;

    if (unlikely(check_mul_overflow(n, size, &bytes)))
        return NULL;

    return kvmalloc(bytes, flags);
}

static inline void *kvcalloc(size_t n, size_t size, gfp_t flags)
{
    return kvmalloc_array(n, size, flags | __GFP_ZERO);
}

extern void kvfree(const void *addr);

内核双链表

当使用 Linux 的双链表接口时,我们应该始终记住这些链表函数不执行锁定,因此我们的设备驱动(或其他内核实体)有可能试图在同一个链表上执行并发操作。这就是为什么我们必须确保实现一个好的锁定方案来保护我们的数据免受竞争条件的影响。

要使用列表机制,我们的驱动必须包含头文件linux/include/linux/list.h;该文件包括标题linux/include/linux/types.h,其中struct list_head类型的简单结构定义如下:

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

我们可以看到,这个结构包含两个指向一个list_head结构的指针(prevnext);这两个指针实现了双向链表功能。然而,有趣的是struct list_head没有像在规范列表实现中那样的专用数据字段。事实上,在 Linux 内核列表实现中,数据字段并没有嵌入列表元素本身;相反,列表结构应该包含在相关的数据结构中。这可能会令人困惑,但实际上并非如此;事实上,为了在我们的代码中使用 Linux 列表工具,我们只需要在使用列表的结构中嵌入一个struct list_head

我们如何将对象结构声明到设备驱动中的一个简单示例如下:

struct l_struct {
    int data;
    ... 
    /* other driver specific fields */
    ...
    struct list_head list;
};

通过这样做,我们创建了一个带有自定义数据的双向链表。然后,为了有效地创建我们的列表,我们只需要使用以下代码声明和初始化列表头:

struct list_head data_list;
INIT_LIST_HEAD(&data_list);

As per other kernel structures, we have the compile time counterpart macro LIST_HEAD(), which can be used to do the same in case of non-dynamic list allocation. In our example, we can do as follows: LIST_HEAD(data_list);

一旦列表头被声明并正确初始化,我们可以使用几个函数,仍然来自linux/include/linux/list.h文件,来添加、移除或进行其他列表条目操作。

如果我们看一下头文件,我们可以看到以下函数在列表中添加或删除元素:

/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
static inline void list_add(struct list_head *new, struct list_head *head);

 * list_del - deletes entry from list.
 * @entry: the element to delete from the list.
 * Note: list_empty() on entry does not return true after this, the entry is
 * in an undefined state.
 */
static inline void list_del(struct list_head *entry);

以下用新条目替换旧条目的功能也是可见的:

/**
 * list_replace - replace old entry by new one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace(struct list_head *old,
                                struct list_head *new);
...

This is just a subset of all available functions. You are encouraged to take a look at the linux/include/linux/list.h file to discover more.

然而,除了可以用来在列表中添加或移除条目的上述函数之外,更有趣的是看到用于创建循环的宏,这些循环遍历列表。例如,如果我们希望以有序的方式添加一个新条目,我们可以这样做:

void add_ordered_entry(struct l_struct *new)
{
    struct list_head *ptr;
    struct my_struct *entry;

    list_for_each(ptr, &data_list) {
        entry = list_entry(ptr, struct l_struct, list);
        if (entry->data < new->data) {
            list_add_tail(&new->list, ptr);
            return;
        }
    }
    list_add_tail(&new->list, &data_list)
}

通过使用list_for_each()宏,我们迭代列表,通过使用list_entry(),我们获得一个指向封闭数据的指针。请注意,我们必须将指向当前元素ptr(我们的结构类型)的指针传递给list_entry(),然后传递给我们的结构中的列表条目的名称(在前面的示例中是list)。

最后,我们可以使用list_add_tail()函数在正确的位置添加新元素。

Note that list_entry() simply uses the container_of() macro to do its job. The macro is explained in Chapter 5, Managing Interrupts and Concurrency, The container_of() macro section.

如果我们再看一下linux/include/linux/list.h文件,我们可以看到更多的函数可以用来从列表中获取一个条目,或者以不同的方式迭代所有列表元素:

/**
 * list_entry - get the struct for this entry
 * @ptr: the &struct list_head pointer.
 * @type: the type of the struct this is embedded in.
 * @member: the name of the list_head within the struct.
 */
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

/**
 * list_first_entry - get the first element from a list
 * @ptr: the list head to take the element from.
 * @type: the type of the struct this is embedded in.
 * @member: the name of the list_head within the struct.
 *
 * Note, that list is expected to be not empty.
 */
#define list_first_entry(ptr, type, member) \
        list_entry((ptr)->next, type, member)

/**
 * list_last_entry - get the last element from a list
 * @ptr: the list head to take the element from.
 * @type: the type of the struct this is embedded in.
 * @member: the name of the list_head within the struct.
 *
 * Note, that list is expected to be not empty.
 */
#define list_last_entry(ptr, type, member) \
        list_entry((ptr)->prev, type, member)
...

一些宏对于遍历每个列表的元素也很有用:

/**
 * list_for_each - iterate over a list
 * @pos: the &struct list_head to use as a loop cursor.
 * @head: the head for your list.
 */
#define list_for_each(pos, head) \
        for (pos = (head)->next; pos != (head); pos = pos->next)

/**
 * list_for_each_prev - iterate over a list backwards
 * @pos: the &struct list_head to use as a loop cursor.
 * @head: the head for your list.
 */
#define list_for_each_prev(pos, head) \
        for (pos = (head)->prev; pos != (head); pos = pos->prev)
...

Again you should note that this is just a subset of all available functions, so you are encouraged to take a look at the linux/include/linux/list.h file to discover more.

内核哈希表

如前所述,对于链表,当使用 Linux 的哈希表接口时,我们应该始终记住这些哈希函数不执行锁定,因此我们的设备驱动(或其他内核实体)可能会尝试在同一个哈希表上执行并发操作。这就是为什么我们必须确保实施一个好的锁定方案来保护我们的数据免受竞争条件的影响。

与内核列表一样,我们可以使用以下代码声明并初始化一个大小为 2 的哈希表:

DECLARE_HASHTABLE(data_hash, bits)
hash_init(data_hash);

As per lists, we have the compile time counterpart macro DEFINE_HASHTABLE(), which can be used to do the same in case of a non-dynamic hash table allocation. In our example, we can use DEFINE_HASHTABLE(data_hash, bits);

这将创建并初始化一个名为data_hash的表,以及基于位的 2 次方大小。如上所述,该表是使用包含内核struct hlist_head类型的桶来实现的;这是因为内核哈希表是使用哈希链实现的,而哈希冲突只是添加到列表的头部。为了更好地理解这一点,我们可以参考DECLARE_HASHTABLE()的宏观定义:

#define DECLARE_HASHTABLE(name, bits) \
    struct hlist_head name[1 << (bits)]

完成后,可以构建一个包含struct hlist_node指针的结构来保存要插入的数据,就像我们之前对列表所做的那样:

struct h_struct {
    int key;
    int data;
    ... 
    /* other driver specific fields */
    ...
    struct hlist_node node;
};

struct hlist_node及其头部struct hlist_headlinux/include/linux/types.h头文件中定义如下:

struct hlist_head {
    struct hlist_node *first;
};

struct hlist_node {
    struct hlist_node *next, **pprev;
};

然后,可以使用如下hash_add()函数将新节点添加到哈希表中,其中&entry.node是数据结构中指向struct hlist_node的指针,key是哈希键:

hash_add(data_hash, &entry.node, key);

关键可以是任何东西;然而,它通常是通过对要存储的数据使用一个特殊的散列函数来计算的。例如,具有 256 个桶的哈希表,可以用下面的hash_func()计算密钥:

u8 hash_func(u8 *buf, size_t len)
{
    u8 key = 0;

    for (i = 0; i < len; i++)
        key += data[i];

    return key;
}

相反的操作,即删除,可以使用hash_del()功能完成,如下所示:

hash_del(&entry.node);

然而,对于列表来说,最有趣的宏是那些用于迭代表的宏。存在两种机制;遍历整个哈希表,返回每个桶中的条目:

hash_for_each(name, bkt, node, obj, member)

另一个只返回对应于密钥散列桶的条目:

hash_for_each_possible(name, obj, member, key)

通过使用最后一个宏,从哈希表中删除节点的过程如下所示:

void del_node(int data)
{
    int key = hash_func(data);
    struct h_struct *entry;

    hash_for_each_possible(data_hash, entry, node, key) {
        if (entry->data == data) {
            hash_del(&entry->node);
            return;
        }
    }
}

Note that this implementation just deletes the first matching entry.

通过使用hash_for_each_possible(),我们可以将列表迭代到与一个键相关的桶中。

以下是linux/include/linux/hashtable.h文件中对hash_add()hash_del()hash_for_each_possible()的定义:

/**
 * hash_add - add an object to a hashtable
 * @hashtable: hashtable to add to
 * @node: the &struct hlist_node of the object to be added
 * @key: the key of the object to be added
 */
#define hash_add(hashtable, node, key) \
        hlist_add_head(node, &hashtable[hash_min(key, HASH_BITS(hashtable))])

/**
 * hash_del - remove an object from a hashtable
 * @node: &struct hlist_node of the object to remove
 */
static inline void hash_del(struct hlist_node *node);

/**
 * hash_for_each_possible - iterate over all possible objects hashing to the
 * same bucket
 * @name: hashtable to iterate
 * @obj: the type * to use as a loop cursor for each entry
 * @member: the name of the hlist_node within the struct
 * @key: the key of the objects to iterate over
 */
#define hash_for_each_possible(name, obj, member, key) \
        hlist_for_each_entry(obj, &name[hash_min(key, HASH_BITS(name))], member)

These are just a subset of all available functions to manage hash tables. You are encouraged to take a look at the linux/include/linux/hashtable.h file to see more.

访问输入/输出内存

为了能够有效地与外设进行通信,我们需要有一种方法来读取和写入其寄存器,为此,我们有两种方法:使用输入/输出端口或使用输入/输出存储器。前一种机制在本书中没有涉及,因为它在现代平台中使用得不多(除了 x86 和 x86_64),而后者只是使用正常的内存区域来映射每个外围寄存器,是现代 CPU 中常用的机制。事实上,I/O 内存映射在片上系统 ( SoC )系统中确实很常见,CPU 只需通过读写知名物理地址,就可以与内部外设进行对话;在这种情况下,每个外设都有自己的保留地址,每个都连接到一个寄存器。

To see a simple example of what I'm talking about, you can get the SAMA5D3 CPU's datasheet from http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11121-32-bit-Cortex-A5-Microcontroller-SAMA5D3_Datasheet_B.pdf; look up page 30, where a complete memory mapping of the whole CPU is reported.

然后在与平台相关的设备树文件中报告该输入/输出内存映射。举个例子,如果我们看一下内核源码的linux/arch/arm64/boot/dts/marvell/armada-37xx.dtsi文件中我们 ESPRESSObin 的 CPU 的 UART 控制器的定义,可以看到如下设置:

soc {
    compatible = "simple-bus";
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    internal-regs@d0000000 {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        /* 32M internal register @ 0xd000_0000 */
        ranges = <0x0 0x0 0xd0000000 0x2000000>;

...

        uart0: serial@12000 {
            compatible = "marvell,armada-3700-uart";
            reg = <0x12000 0x200>;
            clocks = <&xtalclk>;
            interrupts =
            <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>,
            <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>,
            <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
            interrupt-names = "uart-sum", "uart-tx", "uart-rx";
            status = "disabled";
        };

第 4 章所述,使用设备树,我们可以推断出 UART0 控制器被映射到一个物理地址0xd0012000。我们在引导时看到的以下内核消息也证实了这一点:

d0012000.serial: ttyMV0 at MMIO 0xd0012000 (irq = 0, base_baud = 
1562500) is a mvebu-uart

好了,现在我们要记住0xd0012000是 UART 控制器的物理地址,但是我们的 CPU 知道虚拟地址,因为它使用它的 MMU 来获取 RAM 的访问权!那么,如何做好物理地址0xd0012000和虚拟地址之间的转换呢?答案是:通过记忆重映射。在对通用异步收发器控制器的寄存器进行每次读或写操作之前,必须在内核中完成该操作,否则会引发分段错误。

为了了解物理地址和虚拟地址之间的区别以及重新映射操作的行为,我们可以查看名为devmem2的实用程序,该程序可通过http://free-electrons.com/pub/mirror/devmem2.cwget程序在 ESPRESSObin 中下载:

# wget http://free-electrons.com/pub/mirror/devmem2.c

如果我们看一下代码,我们会看到以下操作:

    if((fd = open("/dev/mem", O_RDWR | O_SYNC)) == -1) FATAL;
    printf("/dev/mem opened.\n"); 
    fflush(stdout);

    /* Map one page */
    map_base = mmap(0, MAP_SIZE,
                    PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, target & ~MAP_MASK);
    if(map_base == (void *) -1) FATAL;
    printf("Memory mapped at address %p.\n", map_base); 
    fflush(stdout);

所以devmem2程序只是打开/dev/mem设备,然后调用mmap() 系统调用。该操作将导致在内核源代码中执行linux/ drivers/char/mem.c 文件中的mmap_mem()方法,其中实现了/dev/mem char 设备:

static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;
    phys_addr_t offset = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT;

...

    /* Remap-pfn-range will mark the range VM_IO */
    if (remap_pfn_range(vma,
                        vma->vm_start, vma->vm_pgoff,
                        size,
                        vma->vm_page_prot)) {
        return -EAGAIN;
    }
    return 0;
}

Further information regarding these memory-remap operations and usage of the remap_pfn_range() functions and similar functions will be more clear in Chapter 7, Advanced Char Driver Operations.

好的,mmap_mem()方法将物理地址0xd0012000的内存重新映射操作转换成一个虚拟地址,该虚拟地址适合于被中央处理器用来访问通用异步收发器控制器的寄存器。

如果我们试图用 ESPRESSObin 上的以下命令编译代码,我们将从用户空间获得对 UART 控制器寄存器的可执行的适当访问:

# make CFLAGS="-Wall -O" devmem2 cc -Wall -O devmem2.c -o devmem2

You can safely ignore possible warning messages shown below: devmem2.c:104:33: warning: format '%X' expects argument of type 'unsigned int', but argument 2 has type 'off_t {aka long int}' [-Wformat=] printf("Value at address 0x%X (%p): 0x%X\n", target, virt_addr, read_result ); devmem2.c:104:44: warning: format '%X' expects argument of type 'unsigned int', but argument 4 has type 'long unsigned int' [-Wformat=] printf("Value at address 0x%X (%p): 0x%X\n", target, virt_addr, read_result ); devmem2.c:123:22: warning: format '%X' expects argument of type 'unsigned int', but argument 2 has type 'long unsigned int' [-Wformat=] printf("Written 0x%X; readback 0x%X\n", writeval, read_result); devmem2.c:123:37: warning: format '%X' expects argument of type 'unsigned int', but argument 3 has type 'long unsigned int' [-Wformat=] printf("Written 0x%X; readback 0x%X\n", writeval, read_result);

然后,如果我们执行该程序,我们应该会得到以下输出:

# ./devmem2 0xd0012000 
/dev/mem opened.
Memory mapped at address 0xffffbd41d000.
Value at address 0xD0012000 (0xffffbd41d000): 0xD

如我们所见,devmem2程序按照预期打印重新映射结果,实际读取使用虚拟地址完成,反过来,MMU 在0xd0012000转换为所需的物理地址。

好了,现在很明显,访问外设的寄存器需要内存重映射,我们可以假设一旦我们有一个虚拟地址物理映射到一个寄存器,我们可以简单地引用它来实际读取或写入数据。这是不对的!事实上,尽管内存中映射的硬件寄存器和通常的 RAM 内存之间有很强的相似性,但当我们访问输入/输出寄存器时,我们必须小心避免被 CPU 或编译器优化所欺骗,这些优化可以修改预期的输入/输出行为。

I/O 寄存器和 RAM 的主要区别是 I/O 操作有副作用,而内存操作没有;事实上,当我们将一个值写入 RAM 时,我们期望它被其他人保持不变,但是对于 I/O 内存来说,这是不正确的,因为我们的外设可能会改变寄存器中的一些数据,即使我们将特定的值写入其中。这是一个需要牢记的非常重要的事实,因为为了获得良好的性能,内存内容可以被缓存,读/写指令可以被中央处理器指令流水线重新排序;此外,编译器可以自主决定将数据值放入 CPU 寄存器中,而无需将其写入内存,即使它最终将数据值存储到内存中,写操作和读操作都可以在缓存内存上运行,而永远不会到达物理 RAM。即使它最终将它们存储到内存中,这两种优化在输入/输出内存上都是不可接受的。事实上,这些优化在应用于传统内存时是透明和良性的,但它们对输入/输出操作可能是致命的,因为外设有一种定义明确的编程方式,对其寄存器的读写操作不能被重新排序或缓存而不会导致故障。

这些是我们不能简单地引用虚拟内存地址来读写内存映射外设数据的主要原因。因此,驱动必须确保在访问寄存器时不执行缓存,也不发生读写重新排序;解决方案是使用实际执行读写操作的特殊函数。在linux/include/asm-generic/io.h头文件中,我们可以找到这些函数,如下例所示:

static inline void writeb(u8 value, volatile void __iomem *addr)
{
    __io_bw();
    __raw_writeb(value, addr);
    __io_aw();
}

static inline void writew(u16 value, volatile void __iomem *addr)
{
    __io_bw();
    __raw_writew(cpu_to_le16(value), addr);
    __io_aw();
}

static inline void writel(u32 value, volatile void __iomem *addr)
{
    __io_bw();
    __raw_writel(__cpu_to_le32(value), addr);
    __io_aw();
}

#ifdef CONFIG_64BIT
static inline void writeq(u64 value, volatile void __iomem *addr)
{
    __io_bw();
    __raw_writeq(__cpu_to_le64(value), addr);
    __io_aw();
}
#endif /* CONFIG_64BIT */

The preceding functions are to write data only; you are encouraged to take a look at the header file to see definitions of reading functions, such as readb()readw()readl(), and readq()

根据要操作的寄存器的大小,每个函数被定义为与定义良好的数据类型一起使用;此外,它们中的每一个都使用内存屏障来指示 CPU 以明确定义的顺序执行读写操作。

I'm not going to explain what memory barriers are in this book; if you're curious, you can always read more about it in the kernel documentation directory in the linux/Documentation/memory-barriers.txt file 

作为前面函数的一个简单例子,我们可以看看 Linux 源代码的linux/drivers/watchdog/sunxi_wdt.c文件中的sunxi_wdt_start()函数:

static int sunxi_wdt_start(struct watchdog_device *wdt_dev)
{
...
    void __iomem *wdt_base = sunxi_wdt->wdt_base;
    const struct sunxi_wdt_reg *regs = sunxi_wdt->wdt_regs;

...

    /* Set system reset function */
    reg = readl(wdt_base + regs->wdt_cfg);
    reg &= ~(regs->wdt_reset_mask);
    reg |= regs->wdt_reset_val;
    writel(reg, wdt_base + regs->wdt_cfg);

    /* Enable watchdog */
    reg = readl(wdt_base + regs->wdt_mode);
    reg |= WDT_MODE_EN;
    writel(reg, wdt_base + regs->wdt_mode);

    return 0;
}

一旦获得了寄存器的基址wdt_base和寄存器的映射regs,我们就可以简单地使用readl()writel()来执行我们的读写操作,如前一节所示,我们可以放心它们会被正确执行。

花时间在内核中

第五章管理中断和并发中,我们看到了如何在以后推迟动作;但是,我们可能仍然需要在外围设备上的两次操作之间等待一段时间,如下所示:

writeb(0x12, ctrl_reg);
wait_us(100);
writeb(0x00, ctrl_reg);

也就是说,如果我们必须将一个值写入寄存器,然后等待 100 微秒,然后写入另一个值,这些操作可以通过简单地使用linux/include/linux/delay.h头文件(和其他头文件)中定义的函数来完成,而不是使用之前介绍的技术(内核定时器和工作队列等):

void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

void usleep_range(unsigned long min, unsigned long max);
void msleep(unsigned int msecs);
unsigned long msleep_interruptible(unsigned int msecs);
void ssleep(unsigned int seconds);

所有这些功能只是用来延迟特定的时间量,以纳米、微米或毫秒(或者只是以秒为单位,如ssleep())表示。

第一组函数(即*delay()函数)可以在中断或进程上下文中的任何地方使用,而第二组函数必须在进程上下文中使用,因为它们可能隐式进入睡眠状态。

此外,我们看到,例如,usleep_range()功能通过允许高分辨率定时器利用已经安排好的中断,而不是仅仅为这个睡眠安排一个新的中断,来花费最小和最大的睡眠时间来降低功耗。以下是linux/kernel/time/timer.c文件中的功能描述:

/**
 * usleep_range - Sleep for an approximate time
 * @min: Minimum time in usecs to sleep
 * @max: Maximum time in usecs to sleep
 *
 * In non-atomic context where the exact wakeup time is flexible, use
 * usleep_range() instead of udelay(). The sleep improves responsiveness
 * by avoiding the CPU-hogging busy-wait of udelay(), and the range reduces
 * power usage by allowing hrtimers to take advantage of an already-
 * scheduled interrupt instead of scheduling a new one just for this sleep.
 */
void __sched usleep_range(unsigned long min, unsigned long max);

同样,在同一个文件中,我们看到msleep_interruptible()msleep()的变体,它可以被信号中断(在等待事件食谱中,在 T5【第 5 章、*管理中断和并发,*我们谈到了这种可能性),返回值只是由于中断而没有休眠的时间,单位为毫秒:

/**
 * msleep_interruptible - sleep waiting for signals
 * @msecs: Time in milliseconds to sleep for
 */
unsigned long msleep_interruptible(unsigned int msecs);

最后,我们还应该注意以下几点:

  • *delay()函数使用时钟速度的 jiffy 估计值(loops_per_jiffy值),并将忙碌地等待足够的循环周期来实现所需的延迟。
  • *delay()如果计算值过低loops_per_jiffy(由于执行定时器中断所花费的时间),或者影响执行循环函数所花费时间的缓存行为,或者由于 CPU 时钟速率的变化,函数可能会提前返回。
  • udelay()是一般首选的 API,ndelay()的级别精度在很多非 PC 设备上可能实际不存在。
  • mdelay()udelay()周围的一个宏包装器,用于在向udelay()传递大参数时考虑可能的溢出。这就是不鼓励使用mdelay()的原因,应该重构代码以允许使用msleep()