Linux 内核设计与实现3:https://developer.aliyun.com/article/1597350
3、操作内存区域
// include/linux/mm.h struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr); struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev); static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr);
(1)mmap 和 do_mmap 创建地址区间
(a)用户空间
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
(b)内核空间响应
// include/linux/mm.h static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) { unsigned long ret = -EINVAL; if ((offset + PAGE_ALIGN(len)) < offset) goto out; if (!(offset & ~PAGE_MASK)) ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT); out: return ret; }
(2)munmap 和 do_mummap 删除地址区间
(a)用户空间
#include <sys/mman.h> int munmap(void *addr, size_t length);
(b)内核空间响应
// include/linux/mm.h int do_munmap(struct mm_struct *, unsigned long, size_t); // mm/nommu.c int do_munmap(struct mm_struct *mm, unsigned long start, size_t len) { struct vm_area_struct *vma; struct rb_node *rb; unsigned long end = start + len; int ret; kenter(",%lx,%zx", start, len); if (len == 0) return -EINVAL; /* find the first potentially overlapping VMA */ vma = find_vma(mm, start); if (!vma) { static int limit = 0; if (limit < 5) { printk(KERN_WARNING "munmap of memory not mmapped by process %d" " (%s): 0x%lx-0x%lx\n", current->pid, current->comm, start, start + len - 1); limit++; } return -EINVAL; } /* we're allowed to split an anonymous VMA but not a file-backed one */ if (vma->vm_file) { do { if (start > vma->vm_start) { kleave(" = -EINVAL [miss]"); return -EINVAL; } if (end == vma->vm_end) goto erase_whole_vma; rb = rb_next(&vma->vm_rb); vma = rb_entry(rb, struct vm_area_struct, vm_rb); } while (rb); kleave(" = -EINVAL [split file]"); return -EINVAL; } else { /* the chunk must be a subset of the VMA found */ if (start == vma->vm_start && end == vma->vm_end) goto erase_whole_vma; if (start < vma->vm_start || end > vma->vm_end) { kleave(" = -EINVAL [superset]"); return -EINVAL; } if (start & ~PAGE_MASK) { kleave(" = -EINVAL [unaligned start]"); return -EINVAL; } if (end != vma->vm_end && end & ~PAGE_MASK) { kleave(" = -EINVAL [unaligned split]"); return -EINVAL; } if (start != vma->vm_start && end != vma->vm_end) { ret = split_vma(mm, vma, start, 1); if (ret < 0) { kleave(" = %d [split]", ret); return ret; } } return shrink_vma(mm, vma, start, end); } erase_whole_vma: delete_vma_from_mm(vma); delete_vma(mm, vma); kleave(" = 0"); return 0; } EXPORT_SYMBOL(do_munmap);
十三、页高速缓存和页回写
1、页高速缓存
(1)address_space 结构
address_space 描述了页高速缓存。或者叫 page_cache_entiry 或者 physical_pages_of_a_file 。
// include/linux/fs.h struct address_space { struct inode *host; /* owner: inode, block_device */ struct radix_tree_root page_tree; /* radix tree of all pages */ spinlock_t tree_lock; /* and lock protecting it */ unsigned int i_mmap_writable;/* count VM_SHARED mappings */ struct prio_tree_root i_mmap; /* tree of private and shared mappings */ struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */ spinlock_t i_mmap_lock; /* protect tree, count, list */ unsigned int truncate_count; /* Cover race condition with truncate */ unsigned long nrpages; /* number of total pages */ pgoff_t writeback_index;/* writeback starts here */ const struct address_space_operations *a_ops; /* methods */ unsigned long flags; /* error bits/gfp mask */ struct backing_dev_info *backing_dev_info; /* device readahead, etc */ spinlock_t private_lock; /* for use by the address_space */ struct list_head private_list; /* ditto */ struct address_space *assoc_mapping; /* ditto */ } __attribute__((aligned(sizeof(long))));
(2)address_space_operations
a_ops 域指向地址空间对象中的操作函数表,这与 VFS 对象及其操作表关系类似。
// include/linux/fs.h struct address_space_operations { int (*writepage)(struct page *page, struct writeback_control *wbc); int (*readpage)(struct file *, struct page *); void (*sync_page)(struct page *); /* Write back some dirty pages from this mapping. */ int (*writepages)(struct address_space *, struct writeback_control *); /* Set a page dirty. Return true if this dirtied it */ int (*set_page_dirty)(struct page *page); int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages); int (*write_begin)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata); int (*write_end)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata); /* Unfortunately this kludge is needed for FIBMAP. Don't use it */ sector_t (*bmap)(struct address_space *, sector_t); void (*invalidatepage) (struct page *, unsigned long); int (*releasepage) (struct page *, gfp_t); ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov, loff_t offset, unsigned long nr_segs); int (*get_xip_mem)(struct address_space *, pgoff_t, int, void **, unsigned long *); /* migrate the contents of a page to the specified target */ int (*migratepage) (struct address_space *, struct page *, struct page *); int (*launder_page) (struct page *); int (*is_partially_uptodate) (struct page *, read_descriptor_t *, unsigned long); int (*error_remove_page)(struct address_space *, struct page *); };
2、flusher 线程
flusher 线程后台例程会被周期性唤醒,将那些在内存中驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。在系统启动时,内核初始化一个定时器,让它周期地唤醒 flusher 线程,随后使其运行函数 wb_writeback() 。该函数将把所有驻留时间超过 dirty_expire_interval 毫秒(ms )的脏页写回。
系统管理员可以在 /proc/sys/vm 中设置回写相关的参数,也可以通过 sysctl 系统调用设置它们。下图列出了与 pdflush 相关的所有可设置变量。
十四、设备与模块
1、设备类型
在 Linux 以及所有 Unix 系统中,设备被分为以下三种类型:
- 块设备(blkdev)
- 字符设备(cdev)
- 网络设备(ethernet devices)
杂项设备(miscellaneous device,简写为 miscdev),它实际上是个简化的字符设备。
伪设备(pseudo device),最常见的如内核随机数发生器(/dev/random 和/dev/urandom)、空设备(/dev/null)、零设备(/dev/zero)、满设备(/dev/full)、内存设备(/dev/mem)。
2、模块
(1)hello 模块代码
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int hello_init(void) { printk(KERN_ALERT "I bear a charmed life.\n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Out, out, brief candle!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Shakespeare"); MODULE_DESCRIPTION("A Hello, World Module");
(2)编译
(a)放在内核源代码中编译
例如,希望将它放在 drivers/char 目录下。
- 在 drivers/char 目录下建立 hello 目录。
- 在 drivers/char 目录下 Makefile 文件中添加
obj-m += hello/
或者需要外部控制,则可能如下:
obj-$(CONFIG_HELLO) += hello/
- 在 drivers/char/hello 目录下,新建 Makefile 文件,并添加如下内容:
obj-$(CONFIG_HELLO) += hello.o
- 在内核根目录执行 make,进行编译。
(b)放在内核源代码外编译
- 在你自己的源代码树目录中建立 Makefile 文件,并添加如下内容:
obj-m := hello.o
- 编译
模块在内核内和在内核外构建的最大区别在于构建过程。当模块在内核源代码树外围时,你必须告诉 make 如何找到内核源代码文件和基础 Makefile 文件。命令示例如下:
obj-m := hello.o # 要生成的模块名 # hello-objs:= a.o b.o # 生成这个模块名所需要的目标文件 # KDIR := /lib/modules/`uname -r`/build KDIR := /home/liuqz/learnLinux/linux-4.15.18 PWD := $(shell pwd) default: make -C $(KDIR) M=$(PWD) modules clean: rm -rf *.o *.o.cmd *.ko *.mod.c .tmp_versions
(3)安装与卸载模块
# 安装模块 insmod hello.ko # 卸载模块 rmmod hello.ko
(4)模块参数
(a)代码
module_param(name, type, perm); // 示例 static int nbr = 10; module_param(nbr, int, S_IRUGO);
(b)加载模块时传递参数
sudo insmod module_name.ko nbr=4 # 查看信息 dmesg | tail -6
(5)导出符号表
int get_pirate_beard_color(struct pirate *p) { return p->beard.color; } EXPORT_SYMBOL(get_pirate_beard_color); // 导出符号仅对 GPL 兼容的模块可见 // EXPORT_SYMBOL_GPL(get_pirate_beard_color);
3、设备模型
2.6 内核增加了一个引人注目的新特性——统一设备模型(device model)。设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使得系统具有以下优点:
代码重复最小化。
提供诸如引用计数这样的统一机制。
可以列举系统中所有的设备,观察它们的状态,并且查看它们连接的总线。
可以将系统中的全部设备结构以树的形式完整、有效地展现出来包括所有的总线和内部连接。
可以将设备和其对应的驱动联系起来,反之亦然。
可以将设备按照类型加以归类,比如分类为输入设备,而无需理解物理设备的拓扑结构。
可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源。
最后一点是实现设备模型的最初动机。若想在内核中实现智能的电源管理,就需要建立表示系统中设备拓扑关系的树结构。当在树上端的设备关闭电源时,内核必须首先关闭该设备节点以下的(处于叶子上的)设备电源。比如内核需要先关闭一个 USB 鼠标,然后才可关闭 USB 控制器 :同样内核也必须在关闭 PCI 总线前先关闭 USB 控制器。简而言之,若要准确而又高效地完成上述电源管理目标,内核无疑需要一裸设备树。
(1)kobject
// include/linux/kobject.h struct kobject { const char *name; struct list_head entry; struct kobject *parent; struct kset *kset; struct kobj_type *ktype; struct sysfs_dirent *sd; struct kref kref; unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; };
name 指针指向此 kobject 的名称。
parent 指针指向 kobject 的父对象。这样一来,kobject 就会在内核中构造一个对象层次结构,并且可以将多个对象间的关系表现出来。就如你所看到的,这便是 sysfs 的真正面目:一个用户空间的文件系统,用来表示内核中 kobject 对象的层次结构。
sd 指针指向 sysfs_dirent 结构体,该结构体在 sysfs 中表示的就是这个 kobject。从 sysfs 文件系统内部看,这个结构体是表示 kobject 的一个 inode 结构体。
kref 提供引用计数。ktype 和 kset 结构体对 kobject 对象进行描述和分类。在下面的内容中将详细介绍它们。
kobject 通常是嵌人其他结构中的,其单独意义其实并不大。相反,那些更为重要的结构体,比如定义于 <linux/cdev.h> 中的 struct cdev 中才真正需要用到 kobj 结构。
// include/linux/cdev.h /* cdev structure - 该对象代表一个字符设备 */ struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
当 kobject 被嵌人到其他结构中时,该结构便拥有了 kobject 提供的标准功能。更重要的一点是,嵌入 kobject 的结构体可以成为对象层次架构中的一部分。比如 cdev 结构体就可通过其父指针 cdev->kobj.parent 和链表 cdev->kobj.entry 插入到对象层次结构中。
(2)ktype
kobject 对象被关联到一种特殊的类型,即 ktype(kernel object type 的缩写)。ktype 类型为 kobj_type 结构体。
// include/linux/kobject.h struct kobj_type { void (*release)(struct kobject *kobj); const struct sysfs_ops *sysfs_ops; struct attribute **default_attrs; };
ktype 的存在是为了描述一族 kobject 所具有的普遍特性。如此一来,不再需要每个 kobject 都分别定义自己的特性,而是将这些普遍的特性在 ktype 结构中一次定义,然后所有"同类"的 kobject 都能共享一样的特性。
release 指针指向在 kobject 引用计数减至零时要被调用的析构函数。该函数负责释放所有 kobject 使用的内存和其他相关清理工作。
sysfs_ops 变量指向 sysfs_ops 结构体。该结构体描述了 sysfs 文件读写时的特性。有关其细节参见 17.3.9 节。
最后,default_attrs 指向一个 attribute 结构体数组。这些结构体定义了该 kobject 相关的默认属性。属性描述了给定对象的特征,如果该 kobject 导出到 sysfs 中,那么这些属性都将相应地作为文件而导出。数组中的最后一项必须为 NULL 。
(3)kset
kset 是 kobject 对象的集合体。把它看成是一个容器,可将所有相关的 kobject 对象,比如"全部的块设备"置于同一位置。听起来 kset 与 ktype 非常类似,好像没有多少实质内容。那么"为什么会需要这两个类似的东西呢?" kset 可把 kobject 集中到一个集合中,而 ktype 描述相关类型 kobject 所共有的特性,它们之间的重要区别在于:具有相同 ktype 的 kobject 可以被分组到不同的 kset。就是说,在 Linux 内核中,只有少数一些的 ktype,却有多个 kset 。
kobject 的 kset 指针指向相应的 kset 集合。kset 集合由 kset 结构体表示,定义于头文件 <linux/kobject.h> 中 :
// include/linux/kobject.h /** * struct kset - a set of kobjects of a specific type, belonging to a specific subsystem. * * A kset defines a group of kobjects. They can be individually * different "types" but overall these kobjects all want to be grouped * together and operated on in the same manner. ksets are used to * define the attribute callbacks and other common events that happen to * a kobject. * * @list: the list of all kobjects for this kset * @list_lock: a lock for iterating over the kobjects * @kobj: the embedded kobject for this kset (recursion, isn't it fun...) * @uevent_ops: the set of uevent operations for this kset. These are * called whenever a kobject has something happen to it so that the kset * can add new environment variables, or filter out the uevents if so * desired. */ struct kset { struct list_head list; spinlock_t list_lock; struct kobject kobj; const struct kset_uevent_ops *uevent_ops; };
在这个结构中,其中 list 连接该集合(kset)中所有的 kobject 对象,list_lock 是保护这个链表中元素的自旋锁(关于自旋锁的讨论,详见第 10 章),kobj 指向的 koject 对象代表了该集合的基类。uevent_ops 指向一个结构体一一用于处理集合中 kobject 对象的热插拔操作。uevent 就是用户事件(user event)的缩写,提供了与用户空间热插拔信息进行通信的机制。
(4)kobject、ktype 和 kset 的相互关系
上文反复讨论的这一组结构体很容易令人混淆,这可不是因为它们数量繁多(其实只有三个),也不是它们太复杂(它们都相当简单),而是由于它们内部相互交织。要了解 kobject,很难只讨论其中一个结构而不涉及其他相关结构。然而在这些结构的相互作用下,会更有助你深刻理解它们之间的关系。
这里最重要的家伙是 kobject, 它由 struct koject 表示。kobject 为我们引入了诸如引用计数(reference counting)、父子关系和对象名称等基本对象道具,并且是以一个统一的方式提供这些功能。不过 kobject 本身意义并不大,通常情况下它需要被嵌入到其他数据结构中,让那些包含它的结构具有了 kobject 的特性。
kobject 与一个特别的 ktype 对象关联,ktype 由 struct kobj_type 结构体表示,在 koject 中 ktype 字段指向该对象。ktype 定义了一些 kobject 相关的默认特性:析构行为(反构造功能)、sysfs 行为(sysfs 的操作表)以及别的一些默认属性。
kobject 又归入了称作 kset 的集合,kset 集合由 struct kset 结构体表示。kset 提供了两个功能。第一,其中嵌入的 kobject 作为 kobject 组的基类。第二,kset 将相关的 kobject 集合在一起。在 sysfs 中,这些相关的 koject 将以独立的目录出现在文件系统中。这些相关的目录,也许是给定目录的所有子目录,它们可能处于同一个 kset 。
图 17-1 描述了这些数据结构的内在关系。
(5)管理和操作 kobject
当了解了 kobject 的内部基本细节后,我们来看管理和操作它的外部接口了。多数时候,驱动程序开发者并不必直接处理 kobject,因为 kobject 是被嵌入到一些特殊类型结构体中的(就如在字符设备结构体中看到的情形),而且会由相关的设备驱动程序在"幕后"管理。即便如此,kobject 并不是有意在隐藏自己,它可以出现在设备驱动代码中,或者可以在设备驱动子系统本身中使用它。
使用 kobjcet 的第一步需要先来声明和初始化。kobject 通过函数 kobject_init 进行初始化,该函数定义在文件 <linux/kobject.h> 中:
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
该函数的第一个参数就是需要初始化的 kobject 对象,在调用初始化函数前,kobject 必须清空。这个工作往往会在 kobject 所在的上层结构体初始化时完成。如果 kobject 未被清空,那么只需要调用 memset() 即可:
memset (kobj, 0, sizeof (*kobj));
kobject_init 函数的实现如下:
// lib/kobject.c void kobject_init(struct kobject *kobj, struct kobj_type *ktype) { char *err_str; if (!kobj) { err_str = "invalid kobject pointer!"; goto error; } if (!ktype) { err_str = "must have a ktype to be initialized properly!\n"; goto error; } if (kobj->state_initialized) { /* do not error out as sometimes we can recover */ printk(KERN_ERR "kobject (%p): tried to init an initialized " "object, something is seriously wrong.\n", kobj); dump_stack(); } kobject_init_internal(kobj); kobj->ktype = ktype; return; error: printk(KERN_ERR "kobject (%p): %s\n", kobj, err_str); dump_stack(); } EXPORT_SYMBOL(kobject_init);
- kobject_init_internal
static void kobject_init_internal(struct kobject *kobj) { if (!kobj) return; kref_init(&kobj->kref); INIT_LIST_HEAD(&kobj->entry); kobj->state_in_sysfs = 0; kobj->state_add_uevent_sent = 0; kobj->state_remove_uevent_sent = 0; kobj->state_initialized = 1; }
- kref_init
void kref_set(struct kref *kref, int num) { atomic_set(&kref->refcount, num); smp_mb(); } /** * kref_init - initialize object. * @kref: object in question. */ void kref_init(struct kref *kref) { kref_set(kref, 1); }
- INIT_LIST_HEAD
static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; }
(6)引用计数
// include/linux/kref.h struct kref { atomic_t refcount; }; void kref_set(struct kref *kref, int num); void kref_init(struct kref *kref); void kref_get(struct kref *kref); int kref_put(struct kref *kref, void (*release) (struct kref *kref)); // lib/kref.c void kref_set(struct kref *kref, int num) { atomic_set(&kref->refcount, num); smp_mb(); } void kref_init(struct kref *kref) { kref_set(kref, 1); } void kref_get(struct kref *kref) { WARN_ON(!atomic_read(&kref->refcount)); atomic_inc(&kref->refcount); smp_mb__after_atomic_inc(); } int kref_put(struct kref *kref, void (*release)(struct kref *kref)) { WARN_ON(release == NULL); WARN_ON(release == (void (*)(struct kref *))kfree); if (atomic_dec_and_test(&kref->refcount)) { release(kref); return 1; } return 0; } // include/linux/kobject.h // 递增引用计数 struct kobject *kobject_get(struct kobject *kobj); void kobject_put(struct kobject *kobj); // lib/kobject.c void kobject_put(struct kobject *kobj) { if (kobj) { if (!kobj->state_initialized) WARN(1, KERN_WARNING "kobject: '%s' (%p): is not " "initialized, yet kobject_put() is being " "called.\n", kobject_name(kobj), kobj); kref_put(&kobj->kref, kobject_release); } } static void kobject_release(struct kref *kref) { kobject_cleanup(container_of(kref, struct kobject, kref)); } static void kobject_cleanup(struct kobject *kobj) { struct kobj_type *t = get_ktype(kobj); const char *name = kobj->name; pr_debug("kobject: '%s' (%p): %s\n", kobject_name(kobj), kobj, __func__); if (t && !t->release) pr_debug("kobject: '%s' (%p): does not have a release() " "function, it is broken and must be fixed.\n", kobject_name(kobj), kobj); /* send "remove" if the caller did not do it but sent "add" */ if (kobj->state_add_uevent_sent && !kobj->state_remove_uevent_sent) { pr_debug("kobject: '%s' (%p): auto cleanup 'remove' event\n", kobject_name(kobj), kobj); kobject_uevent(kobj, KOBJ_REMOVE); } /* remove from sysfs if the caller did not do it */ if (kobj->state_in_sysfs) { pr_debug("kobject: '%s' (%p): auto cleanup kobject_del\n", kobject_name(kobj), kobj); kobject_del(kobj); } if (t && t->release) { pr_debug("kobject: '%s' (%p): calling ktype release\n", kobject_name(kobj), kobj); t->release(kobj); } /* free name if we allocated it */ if (name) { pr_debug("kobject: '%s': free name\n", name); kfree(name); } }
4、sysfs
sysfs 文件系统是一个处于内存中的虚拟文件系统,它为我们提供了 kobject 对象层次结构的视图。帮助用户能以一个简单文件系统的方式来观察系统中各种设备的拓扑结构。借助属性对象,kobject 可以用导出文件的方式,将内核变量提供给用户读取或写入(可选)。
虽然设备模型的初表是为了方便电源管理而提出的一种设备拓扑结构,但是 sysfs 是颇为意外的收获。为了方便调试,设备模型的开发者决定将设备结构树导出为一个文件系统。这个举措很快被证明是非常明智的,首先 sysfs 代替了先前处于 /proc 下的设备相关文件;另外它为系统对象提供了一个很有效的视图。实际上,sysfs 起初被称为 driverfs,它早于 kobject 出现。最终 sysfs 使得我们认识到一个全新的对象模型非常有利于系统,于是 kobject 应运而生。今天所有 2.6 内核的系统都拥有 sysfs 文件系统,而且几乎都毫无例外的将其挂载在 sys 目录下。
sysfs 的诀窍是把 kobject 对象与目录项(directory entries)紧密联系起来,这点是通过 kobject 对象中的 dentry 字段实现的。回忆第 12 章,dentry 结构体表示目录项,通过连接 kobject 到指定的目录项上,无疑方便地将 kobject 映射到该目录上。从此,把 kobject 导出形成文件系统就变得如同在内存中构建目录项一样简单。 好了,kobject 其实已经形成了一棵树——就是我们心爱的对象模型体系。由于 kobject 被映射到目录项,同时对象层次结构也已经在内存中形成了一棵树,因此 sysfs 的生成便水到渠成般地简单了。
sysfs 的根目录下包含了至少十个目录:block、bus、class、dev、devices、firmware、fs、kernel、module 和 power。 block 目录下的每个子目录都对应着系统中的一个已注册的块设备。反过来,每个目录下又都包含了该块设备的所有分区。bus 目录提供了一个系统总线视图。class 目录包含了以高层功能逻辑组织起来的系统设备视图, dev 目录是已注册设备节点的视图, devices 目录是系统中设备拓扑结构视图,它直接映射出了内核中设备结构体的组织层次。firmware 目录包含了一些诸如 ACPI、EDD、EFI 等低层子系统的特殊树。fs 目录是已注册文件系统的视图。kemel 目录包含内核配置项和状态信息,module 目录则包含系统已加载模块的信息。power 目录包含系统范围的电源管理数据。并不是所有的系统都包含所有这些目录,还有些系统含有其他目录,但在这里尚未提到。
其中最重要要的目录是 devices,该目录将设备模型导出到用户空间。目录结构就是系统中实际的设备拓扑。其他目录中的很多数据都是将 devices 目录下的数据加以转换加工而得。比如,/sys/class/net/ 目录是以注册网络接口这一高层概念来组织设备关系的,在这个目录中可能会有
目录 eth0,它里面包含的 devices 文件其实就是一个指回到 devices 下实际设备目录的符号链接。
随便看看你可访问到的任何 Linux 系统的 sys 目录,这种系统设备视图相当准确和漂亮,而且可以看到 class 中的高层概念与 devices 中的低层物理设备,以及 bus 中的实际驱动程序之间互相联络是非常广泛的。当你认识到这种数据是开放的,换句话说,这是内核中维持系统的很好表示方式时,整个经历都弥足珍贵。
(1)sysfs 中添加和删除 kobject
仅仅初始化 kobject 是不能自动将其导出到 sysfs 中的,想要把 kobject 导入 sysfs,你需要用到函数 kobject_add() :
// include/linux/kobject.h int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...); // lib/kobject.c int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...) { va_list args; int retval; if (!kobj) return -EINVAL; if (!kobj->state_initialized) { printk(KERN_ERR "kobject '%s' (%p): tried to add an " "uninitialized object, something is seriously wrong.\n", kobject_name(kobj), kobj); dump_stack(); return -EINVAL; } va_start(args, fmt); retval = kobject_add_varg(kobj, parent, fmt, args); va_end(args); return retval; } EXPORT_SYMBOL(kobject_add);
kobject 在 sysfs 中的位置取决于 kobject 在对象层次结构中的位置。如果 kobject 的父指针被设置,那么在 sysfs 中 kobject 将被映射为其父目录下的子目录;如果 parent 没有设置,那么 kobject 将被映射为 kset->kobj 中的子目录。如果给定的 kobject 中 parent 或 kset 字段都没有被设置,那么就认为 kobject 没有父对象,所以就会被映射成 sysfs 下的根级目录。这往往不是你所需要的,所以在调用 kobject_add() 前, parent 或 kset 字段应该进行适当的设置。不管怎么样,sysfs 中代表 kobject 的目录名字是由 fmt 指定的,它也接受 printf() 样式的格式化字符串。
辅助函数 kobject_create_and_add() 把 kobject_create() 和 kobject_add() 所做的工作放在一个函数中 :
// include/linux/kobject.h struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
注意 kobject_create_and_add() 函数接受直接的指针 name 作为 kobject 所对应的目录名称,而
kobject_add() 使用 printf() 风格的格式化字符率。
从 sysfs 中删除一个 kobject 对应文件目录,需使用函数 kobject_del() :
// include/linux/kobject.h void kobject_del(struct kobject *kobj)
上述这些函数都定义于文件 lib/kobject.c 中,声明于头文件 <linux/kobject.h> 中。
(2)向 sysfs 中添加文件
我们已经看到 kobject 被映射为文件目录,而且所有的对象层次结构都优雅地、一个不少地映射成 sys 下的目录结构。但是里面的文件是什么? sysfs 仅仅是一个漂亮的树,但是没有提供实际数据的文件。
(a)默认属性
默认的文件集合是通过 kobject 和 kset 中的 ktype 字段提供的。因此所有具有相同类型的 kobject 在它们对应的 sysfs 目录下都拥有相同的默认文件集合。kobj_type 字段含有一个字段——default_attrs,它是一个 attribute 结构体数组。这些属性负责将内核数据映射成 sysfs 中
的文件。
attribute 结构体定义在文件 <linux/sysfs.h> 中 :
// include/linux/sysfs.h /* attribute 结构体 - 内核数据映射成 sysfs 中的文件 */ struct attribute { const char *name; /* 属性名称 */ struct module *owner; /* 所属模块,如果存在 */ mode_t mode; /* 权限 */ #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lock_class_key *key; struct lock_class_key skey; #endif };
其中名称字段提供了该属性的名称,最终出现在 sysfs 中的文件名就是它。owner 字段在存在所属模块的情况下指向其所属的 module 结构体。如果一个模块没有该属性,那么该字段为 NULL。mode 字段类型为 mode_t,它表示了 sysfs 中该文件的权限。对于只读属性而言,如果是所有人都可读它,那么该字段被设为 S_IRUGO;如果只限于所有者可读,则该字段被设置为 S_IRUSR。同样对于可写属性,可能会设置该字段为 S_IRUGO | S_IWUSR。sysfs 中的所有文件和目录的 uid 与 gid 标志均为零。
虽然 default_attrs 列出了默认的展性,sysfs_ops 字段则描述了如何使用它们。sysfs_ops 字段指向了一个定义于文件 <linux/sysfs.h> 的同名的结构体 :
// include/linux/sysfs.h struct sysfs_ops { /* 在读 sysfs 文件时该方法被调用 */ ssize_t (*show)(struct kobject *, struct attribute *attr, char *buffer); /* 在写 sysfs 文件时该方法被调用 */ ssize_t (*store)(struct kobject *,struct attribute *attr, const char *buffer, size_t size); };
当从用户空间读取 sysfs 的项时调用 show() 方法。它会拷贝由 attr 提供的属性值到 buffer 指定的缓冲区中,缓冲区大小为 PAGE_SIZE 字节;在 x86 体系中,PAGE_SIZE 为 4096 字节。该函数如果执行成功,则将返回实际写入 buffer 的字节数;如果失败,则返回负的错误码。
store() 方法在写操作时调用,它会从 buffer 中读取 size 大小的字节,并将其存放入 attr 表示的属性结构体变量中。缓冲区的大小总是为 PAGE_SIZE 或更小些。该函数如果执行成功,则将返回实际从 buffer 中读取的字节数;如果失败,则返回负数的错误码。
由于这组函数必须对所有的属性都进行文件 I/O 请求处理,所以它们通常需要维护某些通用映射来调用每个属性所特有的处理函数。
(b)创建新属性
通常来讲,由 kobject 相关的 ktype 所提供的默认属性是充足的,事实上,因为所有具有相同 ktype 的 kobject,在本质上区别不大的情况下,都应是相互接近的。也就是说,比如对于所有的分区而言,它们完全可以具有同样的属性集合。这不但可以让事情简单,有助于代码合并,还使类似对象在 sysfs 目录中外观一致。
但是,有时在一些特别情况下会碰到特殊的 kobject 实例。它希望(甚至是必须)有自己的属性——也许是通用属性没包含那些需要的数据或者函数。为此,内核为能在默认集合之上,再添加新属性而提供了 sysfs_create_file() 接口:
// fs/sysfs/file.c int sysfs_create_file(struct kobject * kobj, const struct attribute * attr) { BUG_ON(!kobj || !kobj->sd || !attr); return sysfs_add_file(kobj->sd, attr, SYSFS_KOBJ_ATTR); } int sysfs_add_file(struct sysfs_dirent *dir_sd, const struct attribute *attr, int type) { return sysfs_add_file_mode(dir_sd, attr, type, attr->mode); } int sysfs_add_file_mode(struct sysfs_dirent *dir_sd, const struct attribute *attr, int type, mode_t amode) { umode_t mode = (amode & S_IALLUGO) | S_IFREG; struct sysfs_addrm_cxt acxt; struct sysfs_dirent *sd; int rc; sd = sysfs_new_dirent(attr->name, mode, type); if (!sd) return -ENOMEM; sd->s_attr.attr = (void *)attr; sysfs_dirent_init_lockdep(sd); sysfs_addrm_start(&acxt, dir_sd); rc = sysfs_add_one(&acxt, sd); sysfs_addrm_finish(&acxt); if (rc) sysfs_put(sd); return rc; }
这个接口通过 attr 参数指向相应的 attribute 结构体,而参数 kobj 则指定了属性所在的 kobject 对象。在该函数被调用前,给定的属性将被赋值,如果成功,该函数返回零,否则返回负的错误码。
注意,kobject 中 ktype 所对应的 sysfs_ops 操作将负责处理新属性。现有的 show() 和 store() 方法必须能够处理新属性。
除了添加文件外,还有可能需要创建符号连接。在 sysfs 中创建一个符号连接相当简单 :
// fs/sysfs/symlink.c int sysfs_create_link(struct kobject *kobj, struct kobject *target, const char *name) { return sysfs_do_create_link(kobj, target, name, 1); }
该函数创建的符号链接名由 name 指定,链接则由 kobj 对应的目录映射到 target 指定的目录。如果成功该函数返回零,如果失败返回负的错误码。
(c)删除新属性
删除一个属性需通过函数 sysfs_remove_file() 完成 :
// fs/sysfs/file.c void sysfs_remove_file(struct kobject * kobj, const struct attribute * attr) { sysfs_hash_and_remove(kobj->sd, attr->name); }
一旦调用返回,给定的属性将不再存在于给定的 kobject 目录中。另外由 sysfs_create_link() 创建的符号连接可通过函数 sysfs_remove_link() 删除:
// fs/sysfs/symlink.c void sysfs_remove_link(struct kobject * kobj, const char * name) { struct sysfs_dirent *parent_sd = NULL; if (!kobj) parent_sd = &sysfs_root; else parent_sd = kobj->sd; sysfs_hash_and_remove(parent_sd, name); }
调用一旦返回,在 kobj 对应目录中的名为 name 的符号连接将不复存在。
上述的四个函数在文件 <linux/kobject.h> 中声明;sysfs_create_file() 和 sysfs_remove_file() 数定义于文件 fs/sysfs/file.c 中;sysfs_create_link() 和 sysfs_remove_link() 函数定义于文件 fs/sysfs/symlink.c 中。
(d)sysfs 约定
当前 sysfs 文件系统代替了以前需要由 ioctl()(作用于设备节点)和 procfs 文件系统完成的功能。目前,在合适目录下实现 sysfs 属性这样的功能的确别具一格。比如利用在设备映射的 sysfs 目录中添加一个 sysfs 属性,代替在设备节点上实现一新的 ioctl() 。采用这种方法避免了在调用 ioctl() 时使用类型不正确的参数和弄乱 /proc 目录结构。
但是为了保持 sysfs 干净和直观,开发者必须遵从以下约定。
首先,sysfs 属性应该保证每个文件只导出一个值,该值应该是文本形式而且映射为简单 C 类型。其目的是为了避免数据的过度结构化或太凌乱,现在 /proc 中就混乱而不具有可读性。每个文件提供一个值,这使得从命令行读写变得简洁,同时也使 C 语言程序轻易地将内核数据从 sysfs 导入到自身的变量中去。但有些时候,一值一文件的规则不能很有效地表示数据,那么可以将同一类型的多个值放入一个文件中。不过这时需要合理地表述它们,比如利用一个空格也许就可使其意义清晰明了。总的来讲,应考虑 sysfs 属性要映射到独立的内核变量(正如通常所做),而且要记住应保证从用户空间操作简单,尤其是从 shell 操作简单。
其次,在 sysfs 中要以一个清晰的层次组织数据。父子关系要正确才能将 kobject 层次结构直观地映射到 sysfs 树中。另外,kobject 相关属性同样需要正确,并且要记住 kobject 层次结构不仅仅存在于内核,而且也要作为一个树导出到用户空间,所以要保证 sysfs 树健全无误。
最后,记住 sysfs 提供内核到用户空间的服务,这多少有些用户空间的 ABI(应用程序二进制接口)的作用。用户程序可以检测和获得其存在性、位置、取值以及 sysfs 目录和文件的行为。任何情况下都不应改变现有的文件,另外更改给定属性,但保留其名称和位置不变无疑是在自找麻烦。
这些简单的约定保证 sysfs 可为用户空间提供丰富和直观的接口。正确使用 sysfs,其他应用程序的开发者绝不会对你的代码抱有微辞,相反会赞美它。
(3)内核事件层
内核事件层实现了内核到用户的消息通知系统——就是建立在上文一直讨论的 kobject 基础之上。在 2.6.0 版本以后,显而易见,系统确实需要一种机制来帮助将事件传出内核输送到用户空间,特别是对桌面系统而言,因为它需要更完整和异步的系统。为此就要让内核将其事件压到堆栈 :硬盘满了!处理器过热了!分区挂载了!
早期的事件层没有采用 kobject 和 sysfs,它们如过眼烟云,没有存在多久。现在的事件层借助 koject 和 sysfs 实现已证明相当理想。内核事件层把事件模拟为信号——从明确的 koject 对象发出,所以每个事件源都是一个 sysfs 路径。如果请求的事件与你的第一个硬盘相关,那么 /sys/block/had 便是源树。实质上,在内核中我们认为事件都是从幕后的 kobject 对象产生的。
每个事件都被赋予了一个动词或动作字符串表示信号。该字符串会以"被修改过"或"未挂载"等词语来描述事件。
最后,每个事件都有一个可选的负载(payload)。相比传递任意一个表示负载的字符串到用户空间而言,内核事件层使用 sysfs 属性代表负载。
从内部实现来讲,内核事件由内核空间传递到用户空间需要经过 netlink。netlink 是一个用于传送网络信息的多点传送套接字。使用 netlink 意味着从用户空间获取内核事件就如同在套接字上堵塞一样易如反掌。方法就是用户空间实现一个系统后台服务用于监听套接字,处理任何读到的信息,并将事件传送到系统栈里。对于这种用户后台服务来说,一个潜在的目的就是将事件融入 D-BUS 系统。D-BUS 系统已经实现了一套系统范围的消息总线,这种总线可帮助内核如同系统中其他组件一样地发出信号。
在内核代码中向用户空间发送信号使用函数 kobject_uevent():
// lib/kobject_uevent.c int kobject_uevent(struct kobject *kobj, enum kobject_action action) { return kobject_uevent_env(kobj, action, NULL); } EXPORT_SYMBOL_GPL(kobject_uevent);
第一个参数指定发送该信号的 koject 对象。实际的内核事件将包含该 koject 映射到 sysfs 的路径。
第二个参数指定了描述该信号的"动作"或"动词"。实际的内核事件将包含一个映射成枚举类型 kobject_action 的字符串。该函数不是直接提供一个字符串,而是利用一个枚举变量来提高可重用性和保证类型安全,而且也消除了打字错误或其他错误。该枚举变量为:
// lib/kobject_uevent.c static const char *kobject_actions[] = { [KOBJ_ADD] = "add", [KOBJ_REMOVE] = "remove", [KOBJ_CHANGE] = "change", [KOBJ_MOVE] = "move", [KOBJ_ONLINE] = "online", [KOBJ_OFFLINE] = "offline", };
其形式为 KOBJ_foo。当前值包含 KOBJ_MOUNT、KOBJ_UNMOUNT,KOBJ_ADD,KOBJ_REMOVE 和 KOBJ_CHANGE 等,这些值分别映射为字符串 “mount”、“unmount”、“add”、“remove” 和 “change” 等。当这些现有的值不够用时,允许添加新动作。
使用 kobject 和属性不但有利于很好的实现基于 sysfs 的事件,同时也有利于创建新 kojects 对象和属性来表示新对象和数据——它们尚未出现在 sysfs 中。
这两个函数分别定义和声明于文件 lib/kobject_uevent.c 与文件 <linux/kobject.h> 中
5、小结
本章中,我们考察的内核功能涉及设备驱动的实现和设备树的管理,包括模块、kobject(以及相关的 kset 和 ktype)和 sysfs。这些功能对于设备驱动程序的开发者来说是至关重要的,因为这能够让他们写出更为模块化、更为高级的驱动程序。
十五、调试
1、日志等级 printk
printk(KERN_INFO "SMP alternatives: switching to SMP code\n");
2、syslogd 和 klogd
klogd 是用户空间的守护进程,其从记录缓冲区中获取内核消息,再通过 syslogd 守护进程将它们保存在系统日志文件中。klogd 程序既可以从 /proc/kmsg 文件中,也可以通过 syslog() 系统调用读取这些消息。
syslogd 守护进程把它接收到的所有消息添加进一个文件中,该文件默认是 /var/log/messages 。也可以通过 /etc/syslog.conf 配置文件重新指定。
在启动 klogd 的时候,可以通过指定 -c 标志来改变终端的记录等级。
3、oops
oops 是内核告知用户有不幸发生的最常用的方式。
(1)ksymoops
(2)kallsyms
4、内核调试配置选项
5、引发 bug 并打印信息
BUG() 和 BUG_ON()
6、系统请求键 SysRq
7、内核调试
gdb 和 kgdb