Kernel Inside

声明: 资料来源较零散,很多已经难找到原出处,故所有摘录不做特别出处!

 

Reference

$Kernel/Documentation/arm/*
$Kernel/Documentation/zh_CN/arm/*

Data Structure

ssize_t  & size_t

    为了增强程序的可移植性,便有了size_t,它是为了方便系统之间的移植而定义的,不同的系统上,定义size_t可能不一样。 在32位系统上 定义为 unsigned int 也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long 也就是说在64位系统上是64位无符号整形。
    size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是 size_t, 该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。 所以,它在数组下标和内存管理函数之类的地方广泛使用。
    ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是signed size_t类型的。
 

task_struct内核栈大小

task_struct中有一个成员:void *stack;就是指向下面的内核栈结构体的“栈底”;
内核栈结构体union thread_union 其中的stack成员就是内核栈

在进程被创建的时候,fork族的系统调用中会分别为内核栈和struct task_struct分配空间,调用过程是:
fork族的系统调用--->do_fork--->copy_process--->dup_task_struct
dup_task_struct调用alloc_task_struct and alloc_thread_info

其中alloc_task_struct使用内核的slab分配器去为所要创建的进程分配struct task_struct的空间
而alloc_thread_info使用内核的伙伴系统去为所要创建的进程分配内核栈(union thread_union )空间

由于是每一个进程都分配一个内核栈空间,所以不可能分配很大。这个大小是构架相关的,一般以页为单位。其实也就是上面我们看到的THREAD_SIZE,这个值一般为4K或者8K。对于ARM构架,这个定义在Thread_info.h (arch\arm\include\asm),
    #define THREAD_SIZE_ORDER    1
    #define THREAD_SIZE     8192
    #define THREAD_START_SP     (THREAD_SIZE - 8)

所以ARM的内核栈是8KB
 

List 双链表

 

Bi-direction list

 

For graphic show refer to http://www.makelinux.net/ldd3/chp-11-sect-5OR searching

“The list_head data structure”. Benote head node does not include in custom structure!

 

/*For custom structure including alist_head*/

struct alist {

      list_headlist;

      inta;

}

 

#include/linux/types.h

185 struct list_head {

186         structlist_head *next, *prev;

187 };

 

#include/linux/list.h

21 #define LIST_HEAD(name) \

22         structlist_head name = LIST_HEAD_INIT(name)

60 static inline void list_add(struct list_head *new, structlist_head *head)

104 static inline void list_del(struct list_head *entry)

350 #define list_entry(ptr, type, member)          container_of(ptr, type, member)

380 #define list_for_each(pos, head) \

381         for (pos =(head)->next; pos != (head); pos = pos->next)

 

 

SR: Be aware of two things while using head_list structure.

One is structureinclude real data and list head.

Another is distingiush"node" and "head", the former is link point in customstructure, the latter is entrance point(head) of entire list

For example:

 

 19 struct send_msg

 20 {

 21     struct list_head clients;

 22     struct list_head sendlist;

 23     struct list_head peer;

 24     int type;

 25     void *data;

 26     int size;

 27 };    

Tips:

虽然都是list_head结构,如果是header结点一般名字为xxx实际名称,如果是普通结点名字为xxx_list。举个例子,

list_head tasks; ...

list_head cg_list;...

cg_list 为结点,tasks为header,表示连接指向同一个cgroup_css_set的task列表

 

hlist 哈希链表

struct hlist_head {

    struct hlist_node *first;

};

 

struct hlist_node {

    struct hlist_node *next, **pprev;

};

 

哈希列表的冲突解决方式为链表,散列表相同集合的所有数据以链表方式存储在一块。

整个哈希结构分两个部分,1vs1散列表和每个散列表相同集合(应用散列算法后得到相同散列值的数据集合)对应的链表(即这里所指的hlist_head)

SR: 若散列表中某一散列集合只有一个数据,依然使用链表方式存储,只是链表只有一个元素罢了

整个散列结构定义:hlist head hash list[N];

其中每个hlist head指向一个由hlist_node组成的链表

 

这个数据结构与一般的hash-list数据结构定义有以下的区别:

1) 首先,hash的头节点仅存放一个指针,也就是first指针,指向的是list的头结点,没有tail指针也就是指向list尾节点的指针,这样的考虑是为了节省空间——尤其在hash bucket很大的情况下可以节省一半的指针空间.

2) list的节点有两个指针,但是需要注意的是pprev是指针的指针,它指向的是前一个节点的next指针;其中首元素的pprev指向链表头的fist字段,末元素的next为NULL. (见图).

 

现在疑问来了:为什么pprev不是prev也就是一个指针,用于简单的指向list的前一个指针呢?这样即使对于first而言,它可以将prev指针指向list的尾结点.

 

主要是基于以下几个考虑:

1) hash-list中的list一般元素不多(如果太多了一般是设计出现了问题),即使遍历也不需要太大的代价,同时需要得到尾结点的需求也不多.

2) 如果对于一般节点而言,prev指向的是前一个指针,而对于first也就是hash的第一个元素而言prev指向的是list的尾结点,那么在删除一个 元素的时候还需要判断该节点是不是first节点进行处理.而在hlist提供的删除节点的API中,并没有带上hlist_head这个参数,因此做这 个判断存在难度.

3) 以上两点说明了为什么不使用prev,现在来说明为什么需要的是pprev,也就是一个指向指针的指针来保存前一个节点的next指针--因为这样做即使 在删除的节点是first节点时也可以通过*pprev = next;直接修改指针的指向.来看删除一个节点和修改list头结点的两个API:

static inline voidhlist_add_head(struct hlist_node *n, struct hlist_head *h)

{

    struct hlist_node *first = h->first;

    n->next = first;

    if (first)

        first->pprev = &n->next;

    h->first = n;

    n->pprev = &h->first; //此时n是hash的first指针,因此它的pprev指向的是hash的first指针的地址

}

 

static inline void__hlist_del(struct hlist_node *n)

{

    struct hlist_node *next = n->next;

    struct hlist_node **pprev = n->pprev;

    *pprev = next; // pprev指向的是前一个节点的next指针,而当该节点是first节点时指向自己,因此两种情况下不论该节点是一般的节点还是头结点都可以通过这个操作删除掉所需删除的节点

    if (next)

        next->pprev = pprev;

}

 

 

Bootup

overview

 

$kernel/init/main.c

start_kernel() ...

 

Device Driver Initialization sequence

include/linux/init.h

117 #define pure_initcall(fn)__define_initcall("0",fn,1)
118
119 #define core_initcall(fn) __define_initcall("1",fn,1)
120 #define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
121 #define postcore_initcall(fn) __define_initcall("2",fn,2)
......

 

__define_initcall宏用来将指定的函数指针放到.initcall.init节里。

vmlinux.lds是存在于arch/<target>/目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。

各个子节的顺序是确定的,即先调用.initcall1.init中的函数指针再调用.initcall2.init中的函数指针,等等。不同的入口函数被放在不同的子节中,因此也就决定了它们的调用顺序。

 

那些入口函数的调用由do_initcalls函数(in init/main.c)来完成。

See details in

Vmlinux.lds.h (include/asm-generic)

 

Rootfs-ramdisk

Cmdline中的“initrd=0x01100000,16m”即为ramdisk在内存中的地址

装载rootfs在函数populate_rootfs中,(rootfs_initcall(populate_rootfs);)

 

kernel Init process pid 0

start_kernel  

.-> kernel_init

-> init_post 

.-> kernel_execve(init_filename, argv_init, envp_init);

// Rootfs中的init进程启动过程:

 

 

__init函数在区段.initcall.init中还保存了一份函数指针,在初始化内核会通过这些函数指针调用这些__init函数指针,并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等);这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,和1)中所述的这些函数本身在.init.text区段中的顺序无关

 

在2.6内核中,initcall.init区段又分成7个子区段,分别是

.initcall1.init

.initcall2.init

.initcall3.init

.initcall4.init

.initcall5.init

.initcall6.init

.initcall7.init

当需要把函数fn放到.initcall1.init区段时,只要声明core_initcall(fn);

其他的各个区段的定义方法分别是:(include/linux/init.h)

core_initcall(fn)--->.initcall1.init

postcore_initcall(fn) --->.initcall2.init

arch_initcall(fn) --->.initcall3.init

subsys_initcall(fn) --->.initcall4.init

fs_initcall(fn) --->.initcall5.init

device_initcall(fn) --->.initcall6.init

late_initcall(fn)--->.initcall7.init

而与2.4兼容的initcall(fn)则等价于device_initcall(fn)

 

#definemodule_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)

 

各个子区段之间的顺序是确定的,即先调用.initcall1.init中的函数指针,再调用.initcall2.init中的函数指针,等等。而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。

 

内核中,不同的init函数被放在不同的子区段中,因此也就决定了它们的调用顺序。这样也就解决了一些init函数之间必须保证一定的调用顺序的问题。

 

module_init

SR: LKM加载时符号表链接

首先在kernel的符号表中找,

然后在所有module的符号表中依次查找

 

__initlist(init_a, 1);

它是怎么样通过这个宏来实现初始化函数列表的呢?先来看__initlist的定义:

#define __init__attribute__((unused, __section__(".initlist")))

#define __initlist(fn,lvl) /
static initlist_t __init_##fn __init = { /
 magic:    INIT_MAGIC, /
 callback: fn, /
 level:   lvl }

请注意:__section__(".initlist"),这个属性起什么作用呢?它告诉连接器这个变量存放在.initlist区段,如果所有的初始化函数都是用这个宏,那么每个函数会有对应的一个initlist_t结构体变量存放在.initlist区段,也就是说我们可以在.initlist区段找到所有初始化函数的指针。怎么找到.initlist区段的地址呢?

extern u32__initlist_start;
extern u32 __initlist_end;

这两个变量起作用了,__initlist_start是.initlist区段的开始,__initlist_end是结束,通过这两个变量我们就可以访问到所有的初始化函数了。这两个变量在那定义的呢?在一个连接器脚本文件里

 . = ALIGN(4);
 .initlist : {
  __initlist_start = .;
  *(.initlist)
  __initlist_end = .;
 }
这两个变量的值正好定义在.initlist区段的开始和结束地址,所以我们能通过这两个变量访问到所有的初始化函数。

与此类似,内核中也是用到这种方法,所以我们写驱动的时候比较独立,不用我们自己添加代码在一个固定的地方来调用我们自己的初始化函数和退出函数,连接器已经为我们做好了。先来分析一下module_init。定义如下:

#define module_init(x)    __initcall(x);             //include/linux/init.h

#define __initcall(fn) device_initcall(fn)

#definedevice_initcall(fn)                __define_initcall("6",fn,6)

#define __define_initcall(level,fn,id) /

         staticinitcall_t __initcall_##fn##id __used /

        __attribute__((__section__(".initcall" level ".init"))) =fn

如果某驱动想以func作为该驱动的入口,则可以如下声明:module_init(func);被上面的宏处理过后,变成 __initcall_func6__used加入到内核映像的".initcall"区。内核的加载的时候,会搜索".initcall"中的所有条目,并按优先级加载它们,普通驱动程 序的优先级是6。其它模块优先级列出如下:值越小,越先加载。

 

module_init除了初始化加载之外,还有后期释放内存的作用。linuxkernel中有很大一部分代码是设备驱动代码,这些驱动代码都有初始化和反初始化函数,这些代码一般都只执行一次,为了有更有效的利用内存,这些代码所占用的内存可以释放出来。

      linux就是这样做的,对只需要初始化运行一次的函数都加上__init属性,__init 宏告诉编译器如果这个模块被编译到内核则把这个函数放到(.init.text)段,module_exit的参数卸载时同__init类似,如果驱动被 编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,显然__init和__exit对动态加载的模块是无效的,只支持 完全编译进内核。

在kernel初始化后期,释放所有这些函数代码所占的内存空间。连接器把带__init属性的函数放在同一个section里,在用完以后,把整个section释放掉。当函数初始化完成后这个区域可以被清除掉以节约系统内存。Kenrel启动时看到的消息“Freeing unusedkernel memory: xxxk freed”同它有关。

      我们看源码,init/main.c中start_kernel是进入kernel()的第一个c函数,在这个函数的最后一行是rest_init();

static voidrest_init(void)
{
     .....

    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
     unlock_kernel();
     cpu_idle();

    .....
}
创建了一个内核线程,主函数kernel_init末尾有个函数:

 /*
  * Ok, we have completed the initial bootup, and
  * we're essentially up and running. Get rid of the
  * initmem segments and start the user-mode stuff..
  */
 init_post();

这个init_post中的第一句就是free_initmem();就是用来释放初始化代码和数据的。

void free_initmem(void)
{
    if (!machine_is_integrator() &&!machine_is_cintegrator()) {
    free_area((unsigned long)(&__init_begin),
     (unsigned long)(&__init_end),
     "init");
     }
}

接下来就是kernel内存管理的事了。

 

Linux init pid is 1

I think kernel/init/main.c is process id 0, whereas,$Android/system/core/init/init.c is process id 1.

rc启动脚本中的各个状态在pid1的init中做的,比如early-fs, fs, post-fs. Here is source code.

$Android/system/core/init/init.c  main() { action_for_each_trigger(....)   }

 

 

Basic concept

Device Basic

 

Linux Device Model

 

Ref:http://www.cnblogs.com/wwang/archive/2010/12/16/1902721.html

概念

/sys中的目录给我们提供了如何去看整个设备模型的不同的视角。

sysfs正是用户和内核设备模型之间的一座桥梁,通过这个桥梁我们可以从内核中读取信息,也可以向内核里写入信息。

 

devices子目录,Linux的所有设备都可以在这个目录里找到。

block目录是从块设备的角度来组织设备;

bus目录是从系统总线这个角度来组织设备,这里的总线可以是具有实际意义的总线比如PCI总线,也可以是完 全逻辑上的总线比如USB总线;

class目录把看问题的视角提高到了类别的高度,比如PCI设备或者USB设备等;

dev目录的视角是设备节点;

firmware目录包含了一些比较低阶的子系统,比如ACPI、EFI等;

fs目录里看到的是系统支持的所有文件系统;

kernel目录下包含的是一些内核的配置选项;

module目录下包含的是所有内核模块的信息,内核模块实际上和设备之间是有对应关系的,通过这个目录顺藤摸瓜找到devices或者反过来都是可以做到的;

power目录存放的是系统电源管理的数据,用户可以通过它来查询目前的电源状态,甚至可以直接“命令”系统进入休眠等省电模式。

 

在Linux内核里,kobject是组成Linux设备模型的基础,一个kobject对应sysfs里的一个目录。从面向对象的角度来 说,kobject可以看作是所有设备对象的基类,因为C语言并没有面向对象的语法,所以一般是把kobject内嵌到其他结构体里来实现类似的作用,这里的其他结构体可以看作是kobject的派生类。

 

Linux设备模型还有一个重要的数据结构kset。Kset本身也是一个kobject,所以它在sysfs里同样表现为一个目录,但它和 kobject的不同之处在于kset可以看作是一个容器,如果你把它类比为C++里的容器类如list也无不可。Kset之所以能作为容器来使用,其内 部正是内嵌了一个双向链表结构struct list_head。

 

下面这幅图可以用来表示kobject和kset在内核里关系。

 

 

Kobject在Linux内核里的定义如下:

01    struct kobject {

02        const char      *name;

03        struct list_head    entry;

04        struct kobject      *parent;

05        struct kset     *kset;  //用来指向这个kobject所属的kset

06        struct kobj_type    *ktype;

07        struct sysfs_dirent *sd;

08        struct kref     kref; //实现引用计数

09        unsigned int state_initialized:1;

10        unsigned int state_in_sysfs:1;

11        unsigned int state_add_uevent_sent:1;

12        unsigned int state_remove_uevent_sent:1;

13        unsigned int uevent_suppress:1;

14    };

 

1     struct kobj_type{

2         void (*release)(struct kobject *kobj);   //函数指针release是给kref使用的,当引用计数为0这个指针指向的函数会被调用来释放内存。

3         const struct sysfs_ops *sysfs_ops;

4         struct attribute **default_attrs;

5     };

 

实例1

使用default attribute来创建kobject,利用default attribute时,不同属性之间的读属性和写属性操作只能用属性名称来区别。

 

对应sysfs里的目录关系是:

mykobj1/

|-- mykobj2

| |-- name

| `-- val

|-- name

`-- val

代码:

部分代码:

ssize_t my_store(struct kobject *kobj, struct attribute*attr, const char *buffer, size_t size)

{

      struct my_kobj*obj = container_of(kobj, struct my_kobj, kobj);

 

      if(strcmp(attr->name, "val") == 0) {

            sscanf(buffer,"%d", &obj->val);

      }

      return size;

}

kobject_init_and_add(&obj1->kobj, &my_type, NULL,"mykobj1");

 

实例2

使用default attribute来创建kobject和kset

 

对应sysfs里的目录关系是:

my_kset/

|-- mykobj1

|   |-- name

|   `-- val

`-- mykobj2

    |-- name

    `-- val

 

Kset的定义如下:

view sourceprint?

1     struct kset {

2         struct list_head list;

3         spinlock_t list_lock;

4         struct kobject kobj;

5         const struct kset_uevent_ops *uevent_ops;

6     };

 

代码:

部分代码:

my_kset = kset_create_and_add("my_kset", NULL,NULL);

obj1->kobj.kset = my_kset;

kobject_init_and_add(&obj1->kobj, &my_type, NULL,"mykobj1");

这里需要注意的是,kobject_init_and_add参数里的parent都是NULL,在这种情况下,obj1和obj2的父对象由kobject结构里的kset指针决定,在这个实作里就是my_kset。

 

实例3

创建自己的kobj_attribute来灵活对属性进行读写操作

另外,内核已经实现了四种常见的属性:

bus_attribute, class_attribute, device_attribute,driver_attribute.

mykobj/

|-- mygroup

|   |-- name

|   `-- val

|-- name

`-- val

kobj_attribute是内核提供给我们的一种更加灵活的处理attribute的方式,但是它还不够。只有当我们使用 kobject_create来创建kobject时,使用kobj_attribute才比较方便,但大部分情况下,我们是把kobject内嵌到自己 的结构里,此时就无法直接使用内核提供的dynamic_kobj_ktype,因此,我们需要创建自己的kobj_attribute。

 

核心代码:

首先,我们需要定义自己的attribute:

view sourceprint?

1     structmy_attribute {

2             struct attribute attr;

3             ssize_t (*show)(struct my_kobj *obj,struct my_attribute *attr,

4                             char *buf);

5             ssize_t (*store)(struct my_kobj *obj,struct my_attribute *attr,

6                             const char *buf, size_tcount);

7     };

接下来,利用内核提供的宏__ATTR来初始化my_attribute,并建立attribute数组。

view sourceprint?

1     structmy_attribute name_attribute = __ATTR(name, 0444, name_show, NULL);

2     structmy_attribute val_attribute = __ATTR(val, 0666, val_show, val_store);

struct attribute *my_attrs[] = {

      &name_attribute.attr,

      &val_attribute.attr,

      NULL,

};

struct attribute_group my_group = {

      .name = "mygroup",

      .attrs      = my_attrs,

};         

      retval =sysfs_create_files(&obj->kobj,

                  (conststruct attribute **)my_attrs);

      retval =sysfs_create_group(&obj->kobj, &my_group);

完成这个实作之后,你可以用命令echo 2 >/sys/mykobj/val来修改mykobj下的val文件,可以观察到/sys/mykobj/mygroup/val的内容也会变成2,反之亦然。

代码:

 

module的创建

当module被insmod到内核空间时,/sys/module目录下会相应创建一个和模块同名的目录。我们以usb_storage为例,在 执行完sudo modprobe usb_storage之后,sysfs里会产生一个名为usb_storage的目录,其目录结构是:

 

在Linux 2.6内核里,module的插入是由用户程序insmod(modprobe最终也是调用insmod)发起的,但大部分工作还是由内核完成。我们可以用strace来观察一下insmod的流程。

stat64("/sys/module/usb_storage", 0xbfb9a654)= -1 ENOENT (No such file or directory)

open("/lib/modules/2.6.35-24-generic/kernel/drivers/usb/storage/usb-storage.ko",O_RDONLY) = 3

fstat64(3, {st_mode=S_IFREG|0644, st_size=94776, ...}) = 0

mmap2(NULL, 94776, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0)= 0xb776f000

close(3)                               = 0

init_module(0xb776f000, 94776, "")     = 0

munmap(0xb776f000, 94776)              = 0

exit_group(0)                          = ?

从这个流程我们可以看到,insmod在执行时,首先会把module的内容映射到内存里,然后呼叫系统调用init_module来实现真正的工作。

 

如上图所示,这是系统调用init_module的执行路径。因为本文只是讨论设备模型和sysfs,流程图里只涉及到相关的内容。 

1、mod_sysfs_init首先会查询当前模块(本例中是usb_storage)在sysfs中是否已经存在,如果没有,则调用kobject_init_and_add创建之;

2、调用kobject_create_and_add创建holders目录。holders目录用于存放指向其他module的链接,这里的其 他module都是依赖于当前module的。以usb_storage为例,如果我们需要使用ums_XX模块(比如ums_karma或者 ums_freecom等),可以调用sudomodprobe ums_XX来完成加载,因为ums_XX依赖于usb_storage,所以在usb_storage/holders目录下就会创建指向ums_XX 的链接,同时refcnt也会加1;

3、module_param_sysfs_setup用来创建parameters目录,这个目录里的文件对应着当前module的所有参数。在 Linux内核里,module的二进制ko文件中的__param section用来存储当前module的参数,load_module会把这些参数读取到内存结构 里,module_param_sysfs_setup再根据相应的结构来创建paramters目录及其参数文件;

4、module_add_modinfo_attrs用来创建当前module目录下的4个文件:version、srcversion、 refcnt和initstate,其中version和srcversion的信息存储在二进制ko文件的.modinfo section里。对于usb_storage模块来说,并没有指定version,所以不存在version这个文件。顺便罗嗦一句,在Linux内核 里,可以用宏MODULE_VERSION定义版本号,比如MODULE_VERSION("v1.00")定义版本号为v1.00,这里的版本信息完全 是字符串,并无特定格式。srcversion可以由MODULE_INFO(srcversion,xxx)来定义,但一般情况下由modpost默认生成就可以了。refcnt反映当前module的引用计数。initstate反映module的三 种状态:live、coming和going;

5、add_usage_links和holders是密切相关的,但这个函数并不是操作当前module的holders目录。以ums_XX为 例,在ums_XX的加载过程中,其add_usage_links会把自己作为链接加到usb_storage的holders目录下;

6、sections目录对应当前module的二进制ko文件里的section信息,这是通过add_sect_attrs实现的。需要提醒一 下的是,section的命名通常以“.”开头,而以“.”开头的文件在Linux里被认为是隐藏文件,所以如果要察看的话要在ls命令后加"-a"参 数,下面谈到的notes同样需要这样处理;

7、add_notes_attrs用来创建notes目录。ELF文件格式定义了一种名为note的元素,主要用于给二进制文件添加一些标示信 息。通常,我们可以用readelf来察看ELF文件是否包含note section。比如usb_storage,我们可以使用命令“readelfusb-storage.ko -n”察看,其输出如下:

Notes at offset 0x00000034 with length 0x00000024:

Owner Data size Description

GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)

相应的,在/sys/module/usb_storage/notes目录下建立的notes文件就是.note.gnu.build-id。

 

上面的7条流程里,并无创建drivers的地方,那/sys/module/usb_storage下的drivers目录是如何建立的呢?答案在下图:

 

在usb_storage的module_init中,会调用usb_register来注册usb_driver结构,如图中所示,最终会调用module_add_driver来创建drivers目录。

 

module的撤销

module的撤销过程和前文中的创建过程一一对应,在这里就不详细叙述了,请看下图。

 

 

关于sysfs中的其他子目录的创建和撤销,大家也可以很容易的在Linux内核中找到对应的代码,本文不再一一赘述。

 

代码

struct device {

      struct device           *parent;

      structdevice_private   *p;   // Holds the private data of the driver

      struct kobjectkobj;

。。。。。。

}

 

struct device_private {

………..

      void*driver_data;

      struct device*device;

};

 

其中device->p->driver_data用来存放驱动的私有数据,例如kmalloc出来的数据。

一般在设备驱动中定义,并通过dev_set_drvdata将指针存入device结构中,使用时通过dev_get_drvdata来获得驱动私有数据(例如在sysfs的show方法中使用);

 

平台设备

struct platform_device {

      struct device     dev {

            void        *platform_data;   //Platform specific data

      }

      struct resource   * resource;

}

(1)platform_date是驱动自定义数据结构,一般在板级文件中定义,在驱动中通过platform_device->dev->platform_data来访问,或者调用dev_get_platdata(dev);

 

(2)Resource平台设备IO资源,在板级文件中定义,驱动中通过platform_get_resource来获取

 

 

misc_register

misc_deregister

只提供file_operations类型的ops,没有probe

 

设备文件和sysfs文件

前者,例如字符设备文件,例如/dev/rtc0;

后者,例如./sys/devices/virtual/sec/switch

一般通过device_create创建设备文件,同时注册sysfs对应的文件

即device_create -> device_register  -> device_add -> device_create_file

 

另外,单独创建字符设备文件不关联sysfs可以使用cdev_add创建

单独创建sysfs可以通过device_create_file

 

 

设备和驱动的匹配

设备挂接到总线上时,与总线上的所有驱动进行匹配(用bus_type.match进行匹配),
         如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备,挂接到总线上; 如果匹配失败,则只是将该设备挂接到总线上。
------------------------------------------------------------------------------------------------------------
驱动挂接到总线上时,与总线上的所有设备进行匹配(用bus_type.match进行匹配),
         如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备;挂接到总线上; 如果匹配失败,则只是将该驱动挂接到总线上。

 

 

平台驱动注册

platform_driver_register()

-> driver_register()

-> driver_attach()

-> drv->bus->match(dev, drv)       

//匹配方法是用各个Bus的match函数,

//即struct bus_type中的match函数。

//对于平台驱动来说,match函数是platform_match

其根据platform_device.name和platform_driver. id_table.name是否一致

或者根据platform_device.name和platform_driver.name是否一致

来判断驱动和设备是否匹配

 

平台设备注册:

一般在板级文件中,

static struct platform_device misc_sam_dev = {

      .name ="sample_misc_device",

      .dev.platform_data= NULL,

};

platform_device_register(misc_sam_dev);

-> device_add ()

-> bus_probe_device()

-> device_attach()

 

I2C驱动注册过程:

i2c_register_driver

-> driver_register()

-> i2c_device_match()

 

 

 

 

Hotplug/uevent

 

Quote“在2.6内核里,使用了udev来取代hotplug。据udev的作者Greg K.H说,之所以废弃了hotplug原因是sysfs的出现,这个东西会产生非常多的hotplug事件,远远超过了2.4的内核(只要实现了了kobject模型的设备驱动都回产生该事件)。”

Quote“2.6.15之后,/proc/sys/kernel/hotplug会成空的,因为内核通知用户空间的接口变成了netlink,所以最新的udev也采用了netlink接口去写,废弃了/sbin/hotplug或者/sbin/udevsend。udev在2.6.15以后的内核上可以直接通过netlink接听设备事件,sysfs提供了uevent文件,对该文件的“写”可以送出设备事件!”

uevent, user space event

Quote” udev 完全在用户态(userspace) 工作,利用设备加入或移除时内核所发送的hotplug 事件 (event) 来工作。关于设备的详细信息是由内核输出 (export) 到位于 /sys 的 sysfs 文件系统的。所有的设备命名策略、权限控制和事件处理都是在用户态下完成的。与此相反,devfs 是作为内核的一部分工作的”

 

CoW  ( CopyOn Write )

 

Copy-On-Write (写时复制)定义:

在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,而是在新对 象的内存映射表中设置一个指针,指向源对象的位置,并把那块内存的Copy-On-Write位设置为1.在对这个对象执行读操作的时候,内存数据没有变 动,直接执行就可以。在写的时候,才真正将原始对象复制一份到新的地址,修改新对象的内存映射表到这个新的位置,然后往这里写。[其中运用了引用计数来标识内存块被多个对象引用]

 

linux内核在使用fork创建进程时,基本上会使用Copy-On-Write(COW)技术。这里解释一下COW技术以及为什么在fork中使用。

 

Memory Barrier

内存屏障:由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时地反映出来,也就是说当完成对内存的写入操作之后,读取出来的有可能是旧的内容。我们把避免这种现象发生而采取的手段称为内存屏障(MemoryBarrier) 。

内存屏障的分类:

1.    编译器引起的内存屏障

2.    缓存引起的内存屏障

3.    乱序执行引起的内存屏障

 

通过使用volatile 可以避免编译器在优化时把寄存器分配给不必要的内存变量,从而保证对内存的修改立即反映到相关进程。但是在某些情况下,由于涉及的变量比较多,如果把每一个变量声明为volatile显得很烦琐。因此内核中使用另外一个方式来避免编译器优化引起的副作用,就是通过编译器的预处理宏命令,例如#definebarrier() __asm__ __volatile__("": : :"memory")等等

 

Quote from "kouu'shome"

在我看来,内存屏障主要解决了两个问题:单处理器下的乱序问题和多处理器下的内存同步问题。

 

乱序问题的发生情况:比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。
一 般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种 条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。

 

编译器的乱序情况:指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令的情况),指令之间的相关性也是导致流水线阻塞的重要原因。
CPU的乱序执行并不是任意的乱序,而是以保证程序上下文因果关系为前提的。有了这个前提,CPU执行的正确性才有保证。比如:

a++; b=f(a); c--;

由 于b=f(a)这条指令依赖于前一条指令a++的执行结果,所以b=f(a)将在“执行”阶段之前被阻塞,直到a++的执行结果被生成出来;而c--跟前 面没有依赖,它可能在b=f(a)之前就能执行完。(注意,这里的f(a)并不代表一个以a为参数的函数调用,而是代表以a为操作数的指令。C语言的函数 调用是需要若干条指令才能实现的,情况要更复杂些。)

像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果, 而在流水线中阻塞很久,占用流水线的资源。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令进 入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。比如将指令重排为:

a++; c--; b=f(a);

相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。

乱序的后果
乱序执行,有了“保证上下文因果关系”这一前提,一般情况下是不会有问题的。因此,在绝大多数情况下,我们写程序都不会去考虑乱序所带来的影响。
但是,有些程序逻辑,单纯从上下文是看不出它们的因果关系的。比如:

*addr=5; val=*data;

从 表面上看,addr和data是没有什么联系的,完全可以放心的去乱序执行。但是如果这是在某某设备驱动程序中,这两个变量却可能对应到设备的地址端口和 数据端口。并且,这个设备规定了,当你需要读写设备上的某个寄存器时,先将寄存器编号设置到地址端口,然后就可以通过对数据端口的读写而操作到对应的寄存 器。那么这么一来,对前面那两条指令的乱序执行就可能造成错误。
对于这样的逻辑,我们姑且将其称作隐式的因果关系;而指令与指令之间直接的输入输出依赖,也姑且称作显式的因果关系。CPU或者编译器的乱序是以保持显式的因果关系

变为前提的,但是它们都无法识别隐式的因果关系。再举个例子:

obj->data = xxx; obj->ready = 1;

当设置了data之后,记下标志,然后在另一个线程中可能执行:

if (obj->ready) do_something(obj->data);

虽然这个代码看上去有些别扭,但是似乎没错。不过,考虑到乱序,如果标志被置位先于data被设置,那么结果很可能就杯具了。因为从字面上看,前面的那两条指令其实并不存在显式的因果关系,乱序是有可能发生的。

总的来说,如果程序具有显式的因果关系的话,乱序一定会尊重这些关系;否则,乱序就可能打破程序原有的逻辑。这时候,就需要使用屏障来抑制乱序,以维持程序所期望的逻辑

屏障的作用
内存屏障主要有:读屏障、写屏障、通用屏障、优化屏障、几种。
以读屏障为例,它用于 保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响,同属于屏障的某一侧的读操作也不受影响。类似的,写屏障用于限制写操 作。而通用屏障则对读写操作都有作用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能。比如:

tmp = ttt; *addr = 5; mb(); val = *data;

有了内存屏障就了确保先设置地址端口,再读数据端口。而至于设置地址端口与tmp的赋值孰先孰后,屏障则不做干预。

有了内存屏障,就可以在隐式因果关系的场景中,保证因果关系逻辑正确。

多处理器情况
前面只是考虑了单处理器指令乱序的问题,而在多处理器下,除了每个处理器要独自面对上面讨论的问题之外,当处理器之间存在交互的时候,同样要面对乱序的问题。
个处理器(记为a)对内存的写操作并不是直接就在内存上生效的,而是要先经过自身的cache。另一个处理器(记为b)如果要读取相应内存上的新值,先得acache同步到内存,然后bcache再从内存同步这个新值。而如果需要同步的值不止一个的话,就会存在顺序问题。再举前面的一个例子:
  <CPU-a>              <CPU-b>
  obj->data = xxx;
  wmb();               if (obj->ready)
  obj->ready = 1;          do_something(obj->data);


前面也说过,必须要使用屏障来保证CPU-a不发生乱序,从而使得ready标记置位的时候,data一定是有效的。但是在多处理器情况下,这还不够。dataready标记的新值可能以相反的顺序更新到CPU-b上!
其实这种情况在大多数体系结构下并不会发生,不过内核文档memory-barriers.txt举了alpha机器的例子。alpha机器可能使用分列的cache结构,每个cache列可以并行工作,以提升效率。而每个cache列上面缓存的数据是互斥的(如果不互斥就还得解决cache列之间的一致性),于是就可能引发cache更新不同步的问题。
假设cache被分成两列,而CPU-aCPU-b上的dataready都分别被缓存在不同的cache列上。
先是CPU-a更新了cache之后,会发送消息让其他CPUcache来同步新的值,对于dataready的更新消息是需要按顺序发出的。如果 cache只有一列,那么指令执行的顺序就决定了操作cache的顺序,也就决定了cache更新消息发出的顺序。但是现在假设了有两个cache列,可能由于缓存datacache列比较繁忙而使得data的更新消息晚于ready发出,那么程序逻辑就没法保证了。不过好在SMP下的内存屏障在解决指令乱序问题之外,也将cache更新消息乱序的问题解决了。只要使用了屏障,就能保证屏障之前的cache更新消息先于屏障之后的消息被发出。
后就是CPU-b的问题。在使用了屏障之后,CPU-a已经保证data的更新消息先发出了,那么CPU-b也会先收到data的更新消息。不过同样,CPU-b上缓存datacache列可能比较繁忙,导致对data的更新晚于对ready的更新。这里同样会出问题。
所以,在这种情况下,CPU-b也得使用屏障。CPU-a上要使用写屏障,保证两个写操作不乱序,并且相应的两个cache更新消息不乱序。CPU-b上则需要使用读屏障,保证对两个cache单元的同步不乱序。可见,SMP下的内存屏障一定是需要配对使用的。
所以,上面的例子应该改写成:
  <CPU-a>              <CPU-b>
  obj->data = xxx;     if (obj->ready)
  wmb();                   rmb();
  obj->ready = 1;          do_something(obj->data);

CPU-b
上使用的读屏障还有一种弱化版本,它不保证读操作的有序性,叫做数据依赖屏障。顾名思义,它是在具有数据依赖情况下使用的屏障,因为有数据依赖(也就是之前所说的显式的因果关系),所以CPU和编译器已经能够保证指令的顺序。
再举个例子:

  <CPU-a>              <CPU-b>
  init(newval);        p = data;
  <write barrier>      <data dependency barrier>
  data = &newval;      val = *p;


这里的屏障就可以保证:如果data指向了newval,那么newval一定是初始化过的。

 

误区
SMP环境下,内存屏障保证的是一个CPU的多个操作的顺序(被另一个CPU所观察到的顺序),而不保证两个CPU的操作顺序
举例来说,有如下事件序列:
CPU-0a = 5; CPU-0wmb(); CPU-1rmb(); CPU-1: i = a;

假设从时间顺序上看,CPU-0对内存a的写操作“a = 5”发生于CPU-1的读操作“ i = a”之前,并且中间使用了内存屏障,那么在CPU-1上,i一定等于5么?
未必!因为内存屏障并不保证两个CPU的操作顺序。为什么会是这样呢?
一方面,这样的保证没有必要。两个CPU上执行的操作本身是没有关联的,程序没有要求应该谁先谁后。有可能“a = 5”先执行,也有可能“i = a”先执行,这都符合程序逻辑。只是现在这个case恰好“a = 5”先执行而已。
一方面,两个CPU的操作孰先孰后,是无法通过外部时间来度量的。也就是说,“a = 5”先于“i = a”这件事情不能以它们发生的先后顺序来度量。假设,CPU-0执行了“a = 5”,一个CPU主频周期之后,CPU-1要执行“i = a”。这时候CPU-1如何知道“a = 5”这件事情已经发生了呢?它若想知道,唯一的办法只能跟其他CPU同步一下缓存,但是缓存同步的时间显然远远大于一个CPU主频周期。同步完成之后呢?且不说缓存同步导CPU性能变差。的确,现在CPU-1可以知道现在“a = 5”已经发生了,但是“a = 5”到底是发生在同步发起之前还是同步过程中呢?依然没法知道。除非CPU在修改自己的cache的时候给每个内存单元打一个时间戳,并且时间戳层层传递到内存,并且记录下来。(记录时间戳花费的空间可能比元数据还大!)
更进一步,即便有时间戳,假设CPU-0执行“a = 5”CPU-1执行“a = 3”,这两个操作发生在同一个主频周期,如何度量谁先谁后呢?从时间顺序上显然是没法度量的,因为两个操作是同时发生的,没有先后顺序。但是又非得度量其先后顺序不可,最后a到底等于几总该有个结论吧。度量的标准只能是谁先抢到总线、把a的新值从cache更新到内存,谁就是先者。
所以度量内存操作的先后顺序看的是谁先同步到内存(这一步是串行的,不可能同时发生),而不是看操作发生的时间顺序。可能会这样,CPU-0后执行操作,但是由于种种原因先抢到了总线而先把a更新到内存,那么它就是先者。
么,CPU在看到内存屏障指令之后,是不是应该立马flush cache,使得内存同步的顺序跟时间顺序更为趋近呢?CPU也许可以这么做。但是其实意义并不大,无论如何内存同步顺序永远不可能与时间顺序完全一致,毕竟CPU是并行工作的,而内存同步是串行的。并且flush cache的开销是巨大的,因为内存屏障的作用范围不是某次内存操作,而是屏障前的所有内存操作,所以要flush只能flush所有的cache

 

 

IS_ERR() PTR_ERR() ERR_PTR()

 

 

Come from :http://blog.csdn.net/ce123_zhouwei/article/details/8450618

SR:提炼下文的主要内容:

IS_ERR表示指针是否合法,另外两个宏用于返回错误代码(asm-generic/errno-base.h)

对于32-bit的内存地址的最后一页空间(4K)留出来放置错误代码,当内核返回指针值在这个区域就代表指针非法。以下是引用全文。

 

linux内核中的IS_ERR()、PTR_ERR()和ERR_PTR()

 

在看内核源码的时候,经常会遇到IS_ERR,比如在linux/arch/arm/kernel/sys_arm.c中

 

[plain] view plaincopyprint?

 

    asmlinkage intsys_execve(char __user *filenamei, char __user * __user *argv, 

                  char__user * __user *envp, struct pt_regs *regs) 

    { 

        int error; 

        char *filename; 

     

     

        filename =getname(filenamei); 

        error =PTR_ERR(filename); 

        if(IS_ERR(filename)) 

            gotoout; 

        error =do_execve(filename, argv, envp, regs); 

       putname(filename); 

    out: 

        returnerror; 

    } 

 

IS_ERR宏定义在include/linux/err.h,如下所示:

 

[plain] view plaincopyprint?

 

    #ifndef_LINUX_ERR_H 

    #define_LINUX_ERR_H 

     

    #include<linux/compiler.h> 

     

    #include<asm/errno.h> 

     

    /* 

     * Kernel pointershave redundant information, so we can use a 

     * scheme where wecan return either an error code or a dentry 

     * pointer with thesame return value. 

     * 

     * This should be aper-architecture thing, to allow different 

     * error and pointerdecisions. 

     */ 

    #defineIS_ERR_VALUE(x) unlikely((x) > (unsigned long)-1000L) 

     

    static inline void*ERR_PTR(long error) 

    { 

        return (void *)error; 

    } 

     

    static inline longPTR_ERR(const void *ptr) 

    { 

        return (long) ptr; 

    } 

     

    static inline longIS_ERR(const void *ptr) 

    { 

        returnIS_ERR_VALUE((unsigned long)ptr); 

    } 

     

    #endif /*_LINUX_ERR_H */ 

 

            下面我们就来具体分析一下这段代码,看看内核中的巧妙设计思路。

 

            要想明白IS_ERR(),首先理解要内核空间。所有的驱动程序都是运行在内核空间,内核空间虽然很大,但总是有限的,而在这有限的空间中,其最后一个page是专门保留的,也就是说一般人不可能用到内核空间最后一个page的指针。换句话说,你在写设备驱动程序的过程中,涉及到的任何一个指针,必然有三种情况:

 

    有效指针;

    NULL,空指针;

    错误指针,或者说无效指针。

 

而所谓的错误指针就是指其已经到达了最后一个page,即内核用最后一页捕捉错误。比如对于32bit的系统来说,内核空间最高地址0xffffffff,那么最后一个page就是指的0xfffff000~0xffffffff(假设4k一个page),这段地址是被保留的。内核空间为什么留出最后一个page?我们知道一个page可能是4k,也可能是更多,比如8k,但至少它也是4k,所以留出一个page出来就可以让我们把内核空间的指针来记录错误了。内核返回的指针一般是指向页面的边界(4k边界),即ptr & 0xfff == 0。如果你发现你的一个指针指向这个范围中的某个地址,那么你的代码肯定出错了。IS_ERR()就是判断指针是否有错,如果指针并不是指向最后一个page,那么没有问题;如果指针指向了最后一个page,那么说明实际上这不是一个有效的指针,这个指针里保存的实际上是一种错误代码。而通常很常用的方法就是先用IS_ERR()来判断是否是错误,然后如果是,那么就调用PTR_ERR()来返回这个错误代码。因此,判断一个指针是不是有效的,可用如下的方式:

 

#define IS_ERR_VALUE(x) unlikely((x) > (unsignedlong)-1000L)

 

 (unsigned long)-1000L 应该为  (unsigned long)-0x1000L!(因为 -0x1000 才是 0xFFFFF000),这应该是内核的一个bug吧!在2.6.30.4的内核中是这样定义的:

 

[plain] view plaincopyprint?

 

    #defineMAX_ERRNO   4095 

    #defineIS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO) 

 

即判断是不是在(0xfffff000,0xffffffff)之间,因此,可以用IS_ERR()来判断内核函数的返回值是不是一个有效的指针。注意这里用unlikely()的用意!

 

            至于PTR_ERR(), ERR_PTR(),只是强制转换以下而已。现在应该知道为什么我写返回错误码的时候也加个负号如-ENOSYS这样子了。而PTR_ERR()只是返回错误代码,也就是提供一个信息给调用者,如果你只需要知道是否出错,而不在乎因为什么而出错,那你当然不用调用PTR_ERR()了。

 

而我们的错误码的值在内存中定义都是这样的(asm-generic/errno-base.h):

[plain] view plaincopyprint?

 

    ...... 

    #define EPERM        1 /* Operation not permitted */ 

    #define ENOENT       2 /* No such file or directory */ 

    #define ESRCH        3 /* No such process */ 

    #define EINTR        4 /* Interrupted system call */ 

    #define EIO      5 /* I/O error */ 

    #define ENXIO        6 /* No such device or address */ 

    #define E2BIG        7 /* Argument list too long */ 

    #define ENOEXEC      8 /* Exec format error */ 

    #define EBADF        9 /* Bad file number */ 

    #define ECHILD      10 /* No child processes */ 

    #define EAGAIN      11 /* Try again */ 

    #define ENOMEM      12 /* Out of memory */ 

    #define EACCES      13 /* Permission denied */ 

    #define EFAULT      14 /* Bad address */ 

    #define ENOTBLK     15 /* Block device required */ 

    #define EBUSY       16 /* Device or resource busy */ 

    #define EEXIST      17 /* File exists */ 

    #define EXDEV       18 /* Cross-device link */ 

    #define ENODEV      19 /* No such device */ 

    #define ENOTDIR     20 /* Not a directory */ 

    #define EISDIR      21 /* Is a directory */ 

    #define EINVAL      22 /* Invalid argument */ 

    #define ENFILE      23 /* File table overflow */ 

    #define EMFILE      24 /* Too many open files */ 

    #define ENOTTY      25 /* Not a typewriter */ 

    #define ETXTBSY     26 /* Text file busy */ 

    #define EFBIG       27 /* File too large */ 

    #define ENOSPC      28 /* No space left on device */ 

    #define ESPIPE      29 /* Illegal seek */ 

    #define EROFS       30 /* Read-only file system */ 

    #define EMLINK      31 /* Too many links */ 

    #define EPIPE       32 /* Broken pipe */ 

    #define EDOM        33 /* Math argument out of domain of func */ 

    #define ERANGE      34 /* Math result not representable */ 

    ........ 

 

            如果指针指向了最后一个page,那么说明实际上这不是一个有效的指针。这个指针里保存的实际上是一种错误代码。而通常很常用的方法就是先用IS_ERR()来判断是否是错误,然后如果是,那么就调用PTR_ERR()来返回这个错误代码。

 

 

 

GPIO_TO_IRQ

gpio_to_irq的analysis

//Arch/arm/mach-xx/include/mach/gpio.h

#define gpio_to_irq    __gpio_to_irq

 

//drivers/gpio/gpiolib.c

int __gpio_to_irq(unsigned gpio)

{

      struct gpio_chip  *chip;

 

      chip =gpio_to_chip(gpio);

     returnchip->to_irq ? chip->to_irq(chip, gpio - chip->base) : -ENXIO;

}

其中to_irq的回调函数定义在arch/arm/mach-xx/gpio-eic.c

sci_gpio_chip结构的四个变量定义,

例如a_sci_gpio,其中定义了to_irq回调指针为sci_gpio_to_irq,定义如下:

static int sci_gpio_to_irq(struct gpio_chip *chip, unsignedoffset)

{

      returnchip->base + offset + GPIO_IRQ_START;

}

由此看出,irq = gpio +GPIO_IRQ_START;即irq number和gpio number仅仅是简单的映射关系而已

 

 

Linux Kernel Module

Tools

 

insmod lsmod rmmod depmod modinfo modprobe

(1)     modinfocould let me see what modules do I depends on

(2)     modprobewill auto-load dependence modules one time, and modprobe -r filename willuninstall modules including depended ones

(3)     invokingfunction “reuqest_module” to load module in kernel

(4)     #define__init __section(.init.text) __coldnotrace

#define __initdata   __section(.init.data)

以上宏用于模块初始化函数和数据定义

(5)     模块加载后,在/sys/module/*目录下会有相应的目录

(6)     EXPORT_SYMBOL(symbol_name),will be found in /proc/kallsyms

(7)     Modulereference account, try_module_get and module_put

LKM’s dependence

overview

Each module have some other modules it depends on and othermodules depends on it. See more details by checking out kernel’s source codeabout “source_list” and “target_list” of struct module.

Here is brief explanation.

I gusss, once init_module(insmod), kernel will check out allof external symbols used in loaded modules(simplify_symbols), and find whichLKM provides these symbols by seaching symbols’ location in which section(find_symbol).If found it, that’s modules which loaded module depends on. finally, updatesource_list and target_list(add_module_usage).

以上内容再中文解释下,

加载模块核心函数load_module中,调用两个函数module_unload_init(初始化source_list和target_list链表)以及simplify_symbols(解析加载模块中符号);其中后者会对不同符号进行处理,比如kernel已经包含的符号,外部符号等等,这些不同类别的符号存储在不同的section中,具体详见宏,比如:SHN_UNDEF, SHN_LORESERVE, SHN_ABS and so on. 对外部符号,再去检查它属于哪个section,从而知道符号的定义模块,即加载模块依赖的外部模块。

NOTE:这种建立模块依赖关系的方法同编译时的模块modinfo中的依赖关系建立方法肯定是不同的

how to list dependence of each modules

just run “lsmod” as below

galcore 452396 10 - Live 0x0000000000000000

ssipcmisck 15565 4 - Live 0x0000000000000000

seh 17185 1 ssipcmisck, Live 0x0000000000000000

cploaddev 9944 1 seh, Live 0x0000000000000000

msocketk 93547 3 ssipcmisck,seh,cploaddev, Live0x0000000000000000

For example, seh depends on ssipcmisck

 

 

System Call

Overview

所有的操作系统在其内核里都有一些内建的函数,这些函数可以用来完成一些系统级别的功能。Linux系统使用的这样的函数叫做系统调用,英文是systemcall。这些函数代表了从用户空间内核空间的一种转换,例如在用户空间调用open函数,则会在内核空间调用sys_open。系统调用的数字实际上是一个序列号,表示其在系统的一个系统调用表sys_call_table[]中的位置;

 

arch/arm/include/uapi/asm/unistd.h

//System call numbers in Kernel side

usr/include/asm/unistd_32.h

// System call numbers in Userspace side

 

用户空间系统调用的头文件:/usr/include/bits/syscall.h

系统调用的内核函数头文件(包含SYSCALL_DEFINE*宏定义):$KERNEL/include/linux/syscalls.h

系统调用的调用号和内核最终实现函数的对应关系如下,另外也可以看到最终内核实现的函数名:

64-bitarch/arm64/include/asm/unistd32.h

32-bitarch/arm/kernel/calls.S //调用号和实现函数对应表,被entry-common.S包含

注意:ARMARM64在系统调用表中对调用号和实际函数的处理不一样

 

系统调用的实现有两个特别之处:

      1)函数声明中都有asmlinkage限定词,用于通知编译器仅从栈中提取该函数的参数。

      2)系统调用getXXX()在内核中大多被定义为sys_getXXX()。这是Linux中所有系统调用都应该遵守的命名规则。

 

比如系统调用 getpid实际上就是调用内核函数sys_getpid

sys_getpid的实现在./kernel/sys.c,实际上是SYSCALL_DEFINE0(getpid){…}   

 

再如:系统调用sys_reboot对应的内核函数实现函数的查找方法:

grepSYSCALL_DEFINE -rnIs kernel/|grep reboot

- kernel/sys.c:590:SYSCALL_DEFINE4(reboot, int, magic1,int, magic2, unsigned int, cmd,

 

举一个例子来说:open系统调用,库函数最终会调用__syscall(open),宏展开之后为swi #__NR_open,,swi #0x900005触发中断,

中断号0x900005存放在[lr,#-4]地址中,处理器跳转到arch/arm/kernel/entry-common.Svector_swi读取[lr,#-4]地址中的中断号,之后查询arch/arm/kernel/entry-common.S中的sys_call_table系统调用表,该表内容在arch/arm/kernel/calls.S中定义,__NR_open在表中对应的顺序号为

__syscall_start:

...

.long    SYMBOL_NAME(sys_open)            //5

...

sys_call_table[5]中内容传给pc,系统进入sys_open函数,处理实质的open动作

 

arch/arm/kernel/entry-common.S中对sys_call_table进行了定义:

    .type   sys_call_table, #object

ENTRY(sys_call_table)

#include"calls.S"              //calls.S中的内容顺序链接到这里

COMMON System call

Poll

$Kernel/fs/select.c

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsignedint, nfds,

                        long,timeout_msecs)

-> do_sys_poll()

Futex

在Linux下,信号量和线程互斥锁的实现都是通过futex系统调用。

futex(快速用户区互斥的简称)是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。它们第一次出现在内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现。

Futex 是fast userspace mutex的缩写,意思是快速用户空间互斥体。Linux内核把它们作为快速的用户空间的锁和信号量的预制构件提供给开发者。Futex非常基础,借助其自身的优异性能,构建更高级别的锁的抽象,如POSIX互斥体。大多数程序员并不需要直接使用Futex,它一般用来实现像NPTL这样的系统库。

Futex 由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一个进程可以等待直到那个值变成正数。Futex 的操作几乎全部在应用程序空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用futex 的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代价的)内核系统调用。

mmap

mmap将内存,文件或者其它对象(如设备内存)映射进虚拟地址空间,以便让用户空间的各个进程使用

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。

普通文件被映射到进程地址空间后,进程可以向访

问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

 

 

Clock

 

时间单位:秒second;毫秒ms  millisecond;微秒usmicrosecond;纳秒ns nanosecond

kernel/kernel/time/timekeeping.c

内核时间管理模块,提供内核时间访问函数API,如gettimeofday函数的核心调用API
主要数据结构是timekeeper,实现代码未读

jiffies

记录着从电脑开机到现在总共的时钟中断次数,即tick数。jiffies的精度取决于系统的频率。

HZ

Linux系统时钟频率用HZ表示,一般为100,即系统时钟精度为10ms.

Linux核心每隔固定周期会发出timer interrupt (IRQ 0) ,HZ是用来定义每一秒有几次timer interrupts。

NOTE1: 观察/proc/interrupt的timer中断次数,并于一秒后再次观察其值,理论上等于HZ数。命令如下:

cat /proc/interrupts |grep timer && sleep 1 && cat /proc/interrupts | grep timer

Tick

内核通过编程预设系统定时器的频率,即节拍率(tick rate),每一个周期称作一个tick(节拍)

tick的时间间隔为1/HZ,如HZ为100,tick为10ms

NOTE:注意区分,tick是时钟中断(软件编译时确定好的周期),不是硬件的时钟频率

RTC

独立的硬件时钟源,一般有单独电池供电

wall time 通常说的UTC时钟(绝对时间),即启动系统时要么从RTC中获取,要么从网络获得

 

 

Display

Framebuffer

 

framebuffer映射(map)操作,将屏幕缓冲区的物理地址映射到用户空间的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图了。

 

 

File System

Overview

 

supper block -> dentry -> inode

super block存放文件系统相关信息

索引节点inode存放文件的物理信息

目录项存放文件的逻辑信息

sys file system

Example: power sysfs

/sys/power/*

drivers/base/power/sysfs.c

main macro definition: “power_attr”

to be continued…

proc file system

创建proc目录:

struct proc_dir_entry *proc_mkdir(const char *,structproc_dir_entry *)

创建可读写proc节点:

struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,

                                                                        structproc_dir_entry *parent);

创建只读proc节点:

struct proc_dir_entry *create_proc_read_entry(const char*name,

            mode_t mode, struct proc_dir_entry *base,

            read_proc_t *read_proc, void* data)

 

 

举例:

sturct my_device {

           

}

void module_function(struct my_device *dev)

{

struct proc_dir_entry * new_proc_dir;

struct proc_dir_entry * ent;

 

new_proc_dir = proc_mkdir(“NEW”, NULL);

if (new_proc_dir == NULL) {

            ret = -ENOMEM;

            goto out;

}

 

ent = create_proc_entry(“newFile”, S_IRUSR | S_IWUSR | S_IRGRP| S_IWGRP | S_IROTH | S_IWOTH /*0666*/, new_proc_dir);

if (ent == NULL) {

            ret = -ENOMEM:

            goto out;

}

ent->data = dev; //存放私有数据,存放驱动数据

ent->read_proc = NULL; //参考read_proc_t

ent->write_proc = wFunc;

}

 

static int wFunc(struct file *file, const char *buf, unsignedlong count, void *data)

{

            char *kbuf;

            int val;

 

            kbuf =kzalloc(count + 1, GFP_KERNEL);

            if(copy_from_user(kbuf, buf, count))

                        return–EFAULT;

            sscanf(kbuf,“%d”, &val);

            printk(“val=%d”,val);

 

            kfree(kbuf);

}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值