Linux简介

Linux基于UNIX开发,UNIX在1969年由Dennis Ritchie和Ken Thompson两位大师开发。 Linux最早由Linus Torvalds于1991年开发。整个Linux系统由系统调用将用户空间和内核空间联系起来,用户通过系统调用来进入到内核态,以完成对设备的访问和控制。因此从功能上讲,内核主要是为用户提供接口,对计算机硬件资源进行管理。

Linux具备一些非常好的技术支持,包括:

  • 支持动态载入和卸载内核模块
  • 支持SMP
  • 抢占式任务调度
  • 进程和线程不作区分
  • 面向对象设备模型、热插拔支持、用户空间设备文件系统sysfs

构建内核

最新的Linux内核可以通过如下方式获取:

git clone https://github.com/torvalds/linux

可以看到内核下包括如下文件和目录:

Documentation 文档
arch 架构相关代码
block 块IO层代码
crypto 加密API
drivers 设备驱动
firmware 一些驱动会用到的固件
fs 文件系统
include 头文件
init 内核启动代码
ipc 进程间通信代码
kernel 核心子系统,如进程调度
lib 常用库函数
mm 内存管理子系统
net 网络子系统
samples 样本示例代码
scripts 各种脚本
security 安全模块
sound 声音子系统
tools 开发内核实用工具
virt 虚拟化设施
COPYING 许可证
CREDITS 对内核有较多贡献的人列表
Kbuild Kbuild
Kconfig Kbuild配置文件
MAINTAINERS 各子系统维护者
Makefile 主Makefile

在构建内核之前需要对内核进行配置,比如去掉不必要的模块,修改调试级别,如打开动态debug,打开锁调试等等。

make menuconfig

在menuconfig提供的界面中,有如下一些快捷键可以使用:

? 查看帮助
SPC 用于对当前项选中:星号表示编译至内核、M表示编译为模块
ESC 退出
/ 搜索
< > 左右移动
up down 上下移动

配置好内核即可使用如下命令编译安装:

make -j8                                # 8 means start 8 threads
make modules_install
make install

内核调试

动态调试

内核动态调试是通过一个控制文件来控制哪些消息打印出来,哪些消息不打印。要支持这个功能就需要启用如下配置:

  • CONFIG_DYNAMIC_DEBUG

如果开启动态调试,那么pr_debug()/dev_dbg(), print_hex_dump_debug()/print_hex_dump_bytes()就可以动态选择是否打印出来。使用动态调试的好处是,可以控制文件、函数、行号、模块以及格式字符串。控制文件位于<debugfs>/dynamic_debug/control

下面是用于控制的样例:

echo 'file svcsock.c line 1603 +p' > control # 控制打印行
echo 'func get_resources +p' > control       # 控制函数
echo "file drivers/usb/* +p" > control       # 正则表达式控制
cat batch-file > control                     # 批量控制

控制规则如下:

command ::= match-spec* flags-spec
match-spec
'func' string | 'file' string | 'module' string | 'format' string | 'line' line-range
  • func 用函数名匹配
  • file 用文件匹配
  • module 用模块匹配,不要后缀,其中"-"要替换为"_"
  • format 用格式字符串匹配,特殊字符可以用八进制转义得到,也可以用引号包围
  • line 根据指定行号范围匹配
line-range
lineno | -lineno | lineno- | lineno1-lineno2
flags-spec
更改调试状态
  • -移除
  • +添加
  • =设置为指定标志
  • p启用pr_debug()
  • f在打印消息中添加函数名
  • l在打印消息中添加行号
  • m在打印消息中添加模块名
  • t包含线程ID
  • _没有任何标志位置起

对于print_hex_dump_debug()和print_hex_dump_bytes(),只有p操作有意义,其它操作被忽略。

我们可以用^[-+=][flmpt_]+$匹配标志规则部分,要清除所有标志使用=_即可。

我们也可以从控制文件获取相关信息,控制文件的格式如下:

filename:lineno [module]function flags format
flags
表示启用状态,默认是=_表示没有开启。

如果想查看不是处于默认状态的信息可以用如下一个命令。

awk '$3 != "=_"' control

有时候我们希望在插入模块的时候就打开调试。当执行modprobe foo的时候,modprobe会去为foo.params扫描/proc/cmdline,去掉"foo."之后传递给内核。总共有三个地方的参数会传递给内核,并且有执行顺序。

  1. /etc/modprobe.d/*.conf

    options foo dyndbg=+pt
    options foo dyndbg                      # defaults to +p
    
  2. foo.dyndbg as given in boot args

    foo.dyndbg=" func bar +p; func buz +mp "
    
  3. args to modprobe

    modprobe foo dyndbg==pmf # override previous settings
    

这里dyndbg是一个伪参数,每个模块不必自己去定义,相当于系统已经为每个模块定义好了。

打印调试

打印调试可以用到的格式字符如下表所示。

标识符 类型
基本数据类型  
%d or %x int
%u or %x unsigned int
%ld or %lx long
%lu or %lx unsigned long
%lld or %llx long long
%llu or %llx unsigned long long
%zu or %zx size_t
%zd or %zx ssize_t
%p raw pointer
函数指针  
%pF versatile_init+0x0/0x110
%pf versatile_init
%pS versatile_init+0x0/0x110
%pSR versatile_init+0x9/0x110
%ps versatile_init
%pB prev_fn_of_versatile_init+0x88/0x88
缓冲区 星号用具体的长度值替换
%*ph 00 01 02 … 3f
%*phC 0:01:02: … :3f
%*phD 0-01-02- … -3f
%*phN 00102 … 3f
物理地址  
%pa[p] 0x01234567 or 0x0123456789abcdef
DMA地址  
%pad 0x01234567 or 0x0123456789abcdef

驱动开发

编译环境

设备驱动都放在drivers/目录下,下面又细分为不同类型的设备驱动。假定要写一个字符设备驱动,需要修改drivers/char/Makefile以编译新加的驱动。

obj-$(CONFIG_FISHING_POLE) += fishing.o
fishing-objs := fishing-main.o fishing-line.o
EXTRA_CFLAGS += -DTITANIUM_POLE

由于内核是可以配置的,所以需要添加配置选项,修改drivers/char/Kconfig

config FISHING_POLE
        depends on EXAMPLE_DRIVERS && !NO_FISHING_ALLOWED
        select BAIT
        tristate "Fish Master 3000 support"
        default n
        help
          If you say Y here, support for the Fish Master 3000

当然你也可以创建自己的子目录,子目录中Makefile和Kconfig的写法可以参考父目录。

如果只想写一个不加入到内核的驱动,可以这么写Makefile:

obj-m += fishing.o
fishing-objs := fishing-main.o fishing-line.o
EXTRA_CFLAGS += -DTITANIUM_POLE

编译的时候这么写:

make -C /kernel/source/location SUBDIRS=$PWD modules
make modules_install                    # install module
depmod -A                               # add to dependency
insmod module.ko                        # insert module
rmmod module                            # remove module
modprobe module [parameters]            # insert module
modprobe –r modules                     # remove module

设备驱动

Linux对设备分为三种类型,块设备、字符设备、网络设备。块设备以固定块长作为访问单位。字符设备不可寻址,本质上就是字节流。网络设备通过物理适配器提供访问网络的接口。并不是说设备驱动就一定是驱动物理设备,也可是虚拟设备,例如/dev/urandom就是一个随机数发生器。

设备驱动被写作一个模块,类似于用户空间的一个程序。如果编译时以模块形式生成,那么系统启动后可以动态加载或卸载。一个模块的框架如下所示:

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

static int hello_init(void)             /* like main */
{
        printk(KERN_ALERT "I bear a charmed life.\n");
        return 0;
}
static void hello_exit(void)            /* for release resource */
{
        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");

一个模块可以有参数,也可以导出接口,添加参数要使用内核提供的module_param系列接口,导出接口要要使用EXPORT_SYMBOL_GPL接口。所谓导出接口,就是提供一个函数可以被其它模块使用。

Linux设备驱动模型

基本对象

提到设备模型就不得不说道如下几个类:

kobject
可以看作设备基类,每个设备都应该有一个kobject
kref
在kobject中用kref来进行引用计数,也就是说kref提供了一个通用计数机制
ktype
如果我们把kobject中其它字段看作数据成员,那么ktype就是方法成员
kset
同类对象的集合,ktype是为了让同类对象共享方法,而kset只是一个容器,代表一个子系统

每个kobject都有一个名字和一个引用计数,还有一个父亲,以表示在层次中的位置,此外还有一个类型,一个sysfs中的表示。 kobject自身没有什么用,它们都是嵌入到别的数据结构中去,当然任何数据结构也只能嵌入一个kobject。

struct kobject {
    const char              *name;      // 目录名字
    struct list_head        entry;      // head: kset->list
    struct kobject          *parent;    // 父对象
    struct kset             *kset;      // 所属集合
    struct kobj_type        *ktype;     // 所属类型
    struct kernfs_node      *sd;        // 关联对象与sysfs
    struct kref             kref;       // 引用计数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
    struct delayed_work     release;
#endif
    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;     // 禁止发送uevent
};

kset.png

uevent

内核空间的设备和驱动信息通过sysfs文件系统导出到/sys目录。该目录下各子目录说明如下:

block
系统注册的所有块设备
bus
系统中的总线
class
设备分类,按功能分类
dev
注册的设备节点
devices
导出设备模型
firmware
底层子系统,如ACPI, EDD, EFI等
fs
注册的文件系统
kernel
内核配置和状态信息
modules
载入模块信息
power
电源管理数据

向sysfs添加设备节点是通过kobject来实现的,每一个添加的kobject对应一个目录。而文件则是通过属性添加,可以认为一个文件表示一个属性,添加属性一般要实现show()store()两个方法,用于对文件读取和写入。如果利用好sysfs提供的属性,可以避免使用不安全的ioctl以及混乱的/proc系统。

内核事件通过uevent发送给用户,而uevent也是通过kobject来发送的。当然要完整的工作,也离不开用户空间的监听程序。当用户插拔设备的时候,内核检测到设备插拔并发出插拔事件,调用/proc/sys/kernel/hotplug中指定的用户空间应用对事件进行处理。

以device_add为例,该函数的主要工作如下:

  • 如果没有名字,设置设备的名字
  • 设置其kobj的parent,kobj_add()添加kobject到parent下
  • 创建设备sysfs目录下的文件
    • uevent
    • dev:有设备号才会创建
  • device_add_class_symlinks()
    • subsystem:位于设备属性下,指向所属的子系统的符号链接。
    • device:位于设备属性下,有父亲且不是分区时才会创建,指向父设备的符号链接。
    • name:位于子系统属性下,指向设备属性,名字和设备名相同,如果是块设备就不会创建,因为已经在/sys/block下面创建了和设备名相同的符号链接。
  • device_add_attrs()
    • dev->class->dev_groups
    • dev->type->groups
    • dev->groups
    • dev_attr_online
  • bus_add_device()
    • device_add_attrs() 添加总线属性,不同于设备属性
    • bus->dev_groups
    • name:位于总线属性下,指向设备的符号链接
    • subsystem:位于设备属性下,指向总线的符号链接
  • dpm_sysfs_add()
    • 动态PM相关sysfs文件
  • device_pm_add() 将设备添加到PM核心链表
  • blocking_notifier_call_chain()
  • kobject_uevent(&dev->kobj, KOBJ_ADD);
  • bus_probe_device() 为设备探测合适的驱动

kobject_uevent()

要使用netlink发出uevent,必须配置NET,同样,要使用uevent_helper发出uevent,必须配置UEVENT_HELPER。 udev通过netlink监听,mdev则通过uevent_helper监听。假定所有函数都能成功执行,将其简化后如下所示。

int kobject_uevent_env(struct kobject *kobj,
                       enum kobject_action action,
                       char *envp_ext[])
{
    struct kobj_uevent_env *env =
        kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
    // 利用字符串数组将enum转换为字符串
    const char *action_string = kobject_actions[action];
    struct kobject *top_kobj = ...;             // 必须找到kset
    struct kset *kset = top_kobj->kset;
    const struct kset_uevent_ops *uevent_ops = kset->uevent_ops;
    const char *subsystem;

    if (kobj->uevent_suppress)                  // 禁止发出
        return 0;
    if (uevent_ops && uevent_ops->filter)
        if (!uevent_ops->filter(kset, kobj))    // 被过滤
            return 0;

    if (uevent_ops && uevent_ops->name)
        subsystem = uevent_ops->name(kset, kobj);
    else
        subsystem = kobject_name(&kset->kobj);
    if (!subsystem)                             // 必须有子系统
        return 0;

    const char *devpath = kobject_get_path(kobj, GFP_KERNEL);
    add_uevent_var(env, "ACTION=%s", action_string);
    add_uevent_var(env, "DEVPATH=%s", devpath);
    add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
    for (int i = 0; envp_ext && envp_ext[i]; i++) // 额外环境变量
        add_uevent_var(env, "%s", envp_ext[i]);
    if (uevent_ops && uevent_ops->uevent)       // kset操作
        uevent_ops->uevent(kset, kobj, env);

    if (action == KOBJ_ADD)
        kobj->state_add_uevent_sent = 1;
    else if (action == KOBJ_REMOVE)
        kobj->state_remove_uevent_sent = 1;

    mutex_lock(&uevent_sock_mutex);
    add_uevent_var(env, "SEQNUM=%llu",          // 序列号
                   (unsigned long long)++uevent_seqnum);

#ifdef CONFIG_NET
    struct uevent_sock *ue_sk;
    list_for_each_entry(ue_sk, &uevent_sock_list, list) {
        struct sock *uevent_sock = ue_sk->sk;
        struct sk_buff *skb;
        char *scratch;
        size_t len;

        if (!netlink_has_listeners(uevent_sock, 1))
            continue;

        len = strlen(action_string) + strlen(devpath) + 2;
        skb = alloc_skb(len + env->buflen, GFP_KERNEL);

        scratch = skb_put(skb, len);
        sprintf(scratch, "%s@%s", action_string, devpath);

        for (i = 0; i < env->envp_idx; i++) {
            len = strlen(env->envp[i]) + 1;
            scratch = skb_put(skb, len);
            strcpy(scratch, env->envp[i]);
        }

        NETLINK_CB(skb).dst_group = 1;
        netlink_broadcast_filtered(uevent_sock, skb, 0, 1, GFP_KERNEL,
                                   kobj_bcast_filter, kobj);
    }
#endif
    mutex_unlock(&uevent_sock_mutex);

#ifdef CONFIG_UEVENT_HELPER
    if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
        struct subprocess_info *info;
        const char *path = "PATH=/sbin:/bin:/usr/sbin:/usr/bin"
            add_uevent_var(env, "HOME=/");
        add_uevent_var(env, path);
        init_uevent_argv(env, subsystem);
        info = call_usermodehelper_setup(env->argv[0], env->argv,
                                         env->envp, GFP_KERNEL, NULL,
                                         cleanup_uevent_env, env);
        call_usermodehelper_exec(info, UMH_NO_WAIT);
        env = NULL;     /* freed by cleanup_uevent_env */
    }
#endif

exit:
    kfree(devpath);
    kfree(env);
    return 0;
}

向内核提交代码

如果发现内核中存在问题,或者性能可以提高,或者添加新的驱动等待,就可以向内核提交补丁。不过在提交之前必须做好验证工作,首先代码中不能有BUG,代码必须要安照内核标准风格来写,还要做些必要的静态检查等。

关于代码风格,建议先阅读 Linux kernel coding-style。代码风格的检查可以用如下指令检查:

scripts/checkpatch.pl *.patch

如果要检查的不是补丁而是文件,加一个参数-f即可,如果希望对出现问题的代码修复,可以加参数--fix或者--fix-inplace

sparse是Linux常用的一个静态检查工具,ubuntu用户可以用apt-get安装,安装好之后在调用make时传递参数C=2即可。 smatch也是一个静态检查工具,可以通过如下命令获取:

git clone git://repo.or.cz/smatch.git

安装好以后在调用make时传递参数CHECK="smatch -p=kernel"即可。

当一切检查妥当之后就可以生成patch,一般采用如下命令生成:

git format-patch --cover-letter --thread --subject-prefix="PATCH v2" -5

如果只需要生成一个commit的patch,是不需要--cover-letter--thread选项的。注意--cover-letter需要编辑以添加封面信息。如果第一次提交发现有问题,那么在第二次提交的时候就要加上--subject-prefix="PATCH v2"选项。

准备好patch之后就可以通过如下命令向内核发送补丁了:

git send-email --smtp-server /usr/bin/msmtp \
 --from yourname@email.com \
 --to maintainer1@email1.com \
 --to maintainer2@email2.com \
 --cc devel@linuxdriverproject.org \
 --cc linux-kernel@vger.kernel.org *.patch

很显然git用到msmtp工具来发送邮件,在ubuntu上可以通过apt-get安装,配置文件在~/.msmtprc。大致格式如下:

# Set default values for all following accounts.
defaults
logfile ~/.msmtp.log

# gmail
account gmail
protocol smtp
host gmail.com
from mickyching@gmail.com
user mickyching@gmail.com
password PASSWORD
port 25
auth ntlm
syslog LOG_MAIL

# Set a default account
account default : gmail