Linux设备驱动之设备节点

  对于Linux的各种机制,学习它的最佳方法,我认为是先掌握其用法,再去探究其背后的实现。那么,在真正介绍Linux设备驱动程序之前,我们先探讨设备驱动提供了什么,它是怎么被使用的。本节我们会以键盘这一物理设备为例,来探究应用程序是通过键盘驱动程序提供的设备节点去获取按键键值和动作这一过程。

  在计算机系统中分为两类程序,一类是应用程序,负责提供策略,直接为用户服务。比如:Word是一款文本编辑器,当我们新建一个文档,敲击键盘时,Word就会从操作系统中获取到我们敲击键盘对应的字符,并显示在页面中。另一类程序是操作系统,它包括了设备驱动、内存管理、进程调度、文件系统等。Word获取的字符是从操作系统中获取的,那么当敲击键盘时,操作系统又是如何获取到键盘上对应的字符的呢?

一、什么是设备节点

  人和人之间沟通桥梁是语言。同样,应用程序和设备驱动程序沟通也需要一个桥梁。这个桥梁就是设备节点。在Linux设计哲学这篇文章中,我们提到Linux系统的“一切皆文件”的设计思想。意思指对于Linux系统,所有的IO资源都是文件,包括文件、目录、硬盘、设备等。那么,键盘作为计算机系统中的一款输入设备,操作系统同样也把它抽象了文件,要想获取用户从键盘上输入的数据时,只需要读取键盘提供的设备节点即可。
  在Linux系统中,键盘作为输入设备,其对应的设备节点位于"/dev/input"下。在这个文件夹下有很多以event打头的文件,这些就是所有input设备的设备节点。如何确定哪个是键盘的设备节点呢?将键盘连接到树莓派上,打开终端,执行“sudo cat /dev/input/event0”,敲击键盘,如果没有输出,就换下一个节点,直到找到有输出的节点,那这个节点就是键盘对应的设备节点。

二、如何从设备节点中获取数据

  上文中,我们确定了键盘对应的设备节点,在这一小节中,我们将写一个小的程序来从该设备节点中获取按下按键之后设备驱动上报的数据,并最终解析出对应的键值。

  前面也提到过,操作系统之所以把IO都抽象成了文件,最大的好处就是可以通过统一的接口来访问这个文件,从而和不同的设备沟通。这些统一的接口就是操作系统针对文件操作对外提供的一组系统调用:open函数、read函数、write函数等。比如,如果需要从一个设备中获取数据,只需要调用read函数去读取该设备对应的设备节点就可以了,当然在read之前,要先调用open函数打开。现在以获取键盘输入为例来介绍。

2.1 打开设备节点

在读取设备节点的数据之前,要先调用open函数打开设备节点。open函数的具体用法可以参考链接。简单描述如下:

函数声明:
  int open(const char *pathname, int flags);
需要包含的头文件:
  #include
参数:

  • 第一个参数(const char *pathname):表示需要打开的文件路径
  • 第二个参数(int flags):表示打开文件的方式,比如,"O_RDONLY" ——只读打开;"O_WRONLY"——只写打开;"O_RDWR"——读、写打开,等。

返回值:
 如果打开成功,则返回该文件的文件描述符,以供read,write等函数使用。否则,返回-1。

那么,要打开键盘的设备文件(假设是"/dev/input/even10"),则需要以下代码:

    int keys_fd;

    keys_fd = open("/dev/input/even10", O_RDONLY);
    if(keys_fd <= 0)
    {
        printf("open /dev/input/event10 device error!\n");
        return -1;
    }

2.2 读取设备节点的数据

读取设备节点需要使用read函数,具体使用方法可以参考链接。简单介绍如下:

函数声明:
  ssize_t read(int fd, void *buf, size_t count);
需要包含的头文件:
  #include
参数:

  • 第一个参数(int fd):要打开文件的文件描述符,来源一般是上述open函数的返回值。
  • 第二个参数(void *buf):读取到的数据存放的起始位置指针
  • 第三个参数(size_t count):要读取的数据字节数

返回值:

  • 如果读取成功,则返回实际读取到的字节数
  • 如果读取失败,则返回-1
  • 如果返回值小于第三个参数count,则表示已经读取到文件结尾,返回值表示实际读取的字节数。

在读取键盘的例子中,我们循环读取键盘设备的文件节点,并将设备保存到一个char buf[24]的数组中去。具体代码如下:

char buf[24];
while(1)
{
    if(read(keys_fd, buf, 24) == 24)
    {
        // 成功的从设备节点中获取到了24个字节
        ...
    }
}

根据read函数用法,当要读取24个字节,且read函数的返回值是24时,表示成功的从设备节点中获取到了24个字节。

2.3 分析从设备节点获取的数据

为什么这里要从键盘的设备驱动获取24个字节呢?这是因为正常情况下,从键盘设备节点获取的数据实际上是一个struct input_event结构。其定义为:

struct input_event {
    struct timeval time;
    __u16 type;
    __u16 code;
    __s32 value;
};

显然,上述结构体的大小为24

  这里需要理解的是:设备节点是设备驱动程序提供的,且设备节点的数据是设备驱动写入的,而且写入时,是以上述结构的规则写入的,这是双方通过<linux/input.h>约定好的,那么应用程序去设备节点中读取数据之后,也需要按照上述结构去解析数据。那这个结构具体是什么意思呢?

  • struct timeval time:其大小为16个字节,具体意义暂时不考虑。
  • __u16 type:其大小为2个字节,表示input设备的类型,比如:EV_KEY表示上报的是键盘类型的数据,EV_REL表示相对路径,鼠标就属于这种类型,还是其他等等。
  • __u16 code:其大小为2个字节,表示事件的代码。比如,如果typeEV_KEY,那么该代码code为设备键盘代码。code值实际上是应用程序和驱动程序约定好的一些固定的值,它可取的值位于include/uapi/linux/input-event-codes.h中。举例来讲,根据Linux源码下的include/uapi/linux/input-event-codes.h文件的第91行#define KEY_Q 16,如果键盘上按下或松开了Q键,那么键盘的驱动程序上报的code值应该是16;反之,如果应用程序获取到的值是19,那么,表示用户按下或松开了键盘上的Q键。
  • __s32 value:其大小为4个字节,事件的值。如果事件的类型代码是EV_KEY,当按键按下时值为1,松开时值为0

根据上述解释,我们可以添加以下代码来解析从设备节点中获取的数据。

if(t.type == EV_KEY)                // 我们只关心input event类型为EV_KEY(按键)的消息
    if(t.value == 0 || t.value == 1)
    {
        printf("key %d %s\n",
        t.code,                     // t.code表示按下或松开了哪个按键
        (t.value) ? "Pressed" : "Released");   // t.value表示按下还是松开了相应的按键
    }

2.4 关闭设备节点

在从设备节点获取数据完成后,务必调用close函数,来关闭设备节点。即

close(keys_fd);

三、实验演示

将上述程序放到树莓派中,命名为keyboard.c。然后,执行以下命令编译:

gcc -o keyboard keyboard.c

执行sudo ./keyboard,运行程序,然后按下、松开连接于树莓派的键盘上的Q键,会得到如下输出:

pi@raspberrypi:~/Source/keyboard $ sudo ./keyboard
key 16 Pressed
key 16 Released
key 16 Pressed
key 16 Released

这里输出按下或者松开了code为16的按键。打开kernel代码中include/uapi/linux/input-event-codes.h,搜索"KEY_Q",在第91行,我们找到了以下代码:

#define KEY_Q           16

可以确认,16就是按键Q的值。

四、总结

  • 设备驱动程序提供了设备节点;
  • 设备节点作为应用程序和驱动程序(操作系统)沟通的桥梁,用来使双方传输数据;
  • 在用户空间(应用程序中)可以通过操作系统提供的文件系统相关的系统调用(open,read,write等函数)去访问设备节点。
  • 对于input设备来讲,应用程序和驱动程序约定好,以struct input_event来传递数据。

五、参考资料

  1. input 子系统event结构介绍

Linux的模块机制

说明:

  1. 所有代码均在Ubuntu 18.04(内核版本linux 5.4.0-52-generic)上编译运行通过
  2. 本文同步发表于www.linuxrevead.com

一、什么是内核模块

  内核模块实际上是实现能够为内核扩展某个具体功能的、高聚合的代码。它可以引用内核的API,但一般不对外提供API。之所以称其为模块的原因是因为它的代码可以独立于内核之外,编译结果也可以独立于内核镜像vmlinux之外。在使用时,通过某些方法将预先编译好的模块文件放入到内核的文件系统中,通过内核提供的模块相关工具在内核运行时,将该模块从内核中加载或者卸载,从而实现给内核扩展某种功能。

  使用内核模块最显著的特点是Linux kernel运行时即可动态的完成模块的安装和卸载,而并不需要关闭或者重启kernel,这样节省了功能升级成本和服务器切换的时间成本。

二、如何编写内核模块

  在学习编写Linux模块之前,有必要说明一下,初学者只需要记住模块编写的格式,重要函数的作用以及它们被调用的时机(实际上就是Linux内核模块的框架)即可,而并不需要完全理解代码中每一行的意思。等真正能够灵活使用模块,或者对于内核有一定了解,再去理解“Linux内核模块框架为什么是这样的”、“背后的实现逻辑是什么”就会简单很多。这就类似于初学者学习C语言的时候,最开始只是机械的记住“在代码第一行写include ”、“代码中要有且只能有一个main函数”、“程序运行时会从main函数执行”是一个道理。

2.1 编写"Hello world"内核模块

  按照国际惯例,我们首先学习一下"Hello world"内核模块代码。新建一个hello.c文件,在其中写入下列代码:

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

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello world!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Goodbye world!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

我们简单介绍一下这个模块的组成。
  首先,包含了内核其他模块提供的两个头文件,用于声明后面我们要使用到的“module_init”、 “module_exit” 和 “MODULE_LICENSE”。
  其次,声明了“hello_init”函数,在其中输出了“Hello world!”;声明了“hello_exit”函数,在其中输出了“Goodbye world!”。
  最后,“hello_init”函数被“module_init”修饰,表示模块加载的时候被调用。“hello_exit”函数被“module_exit”修饰,表示模块卸载的时候被调用。

  上述模块实现了以下两个功能:

  1. 在模块被加载到内核中的时候,hello_init函数会被调用,从而在内核的log中输出“Hello world!
  2. 在模块从内核中卸载的时候,hello_exit函数会被调用,从而在内核的log中输出“Goodbye world!

2.2 将模块添加到kernel代码中

  由于后面还要基于这个模块编写字符设备驱动,所以,我们现在在drivers/char下新建hello这个文件夹,并把hello.c这个模块放置于drivers/char/hello目录下。Kernel使用Makefile来管理所有的文件,因此如果想在编译kernel的时候,把我们新加入的drivers/char/hello/hello.c一起进去,那么需要修改两个Makefile。

修改Makefile——编译drivers/char/hello目录

drivers/char/Makefile的最后一行添加以下代码:

obj-y               += hello/

表示drivers/char/hello需要进行编译。

修改Makefile——编译drivers/char/hello/hello.c

drivers/char/hello目录下新增Makefile,并加入以下内容:

obj-m               += hello.o

表示drivers/char/hello/hello.c需要进行编译,并将其编译模块。其中,

  • m表示将hello.c编译为模块;
  • m可以修改为y,表示将hello.c默认编入Linux内核镜像(即vmlinux)中;
  • m可以修改为n,表示不编译hello.c

  对于一个模块,我们想配置它时,可以直接修改Makefile,但是kernel中有数百个模块,而且有些模块之间依赖关系,那么用修改Makefile的方法工作量就太大了。Kernel中提供了另外一种机制去配置这些模块是否编译,或者是否编译成模块,即Kconfig。

增加hello模块的Kconfig

如果使用Kconfig来配置hello.c,需要以下步骤:
1)将drivers/char/hello/Makefile中的m替换成一个变量,该变量的命名规则,我们沿用Linux的规则,定义为CONFIG_HELLO
2)在drivers/char/hello/Kconfog中增加以下内容来定义及描述该变量

config HELLO
    tristate "Device driver sample code, named hello"
    default m
    help
        Configure the device driver sample
  • 第一行表示,定义变量CONFIG_HELLO
  • 第二行的tristate表示CONFIG_HELLO变量可以取三个值ymn
  • 第二行的“Device driver sample code, named hello” 表示对该变量简要的解释
  • 第三行的"default m"表示CONFIG_HELLO变量的默认值
  • 第四行“help”及其后的内容表示对CONFIG_HELLO变量的详细解释。

3)在drivers/char/Kconfog增加下面的代码,以包含drivers/char/hello/Kconfog

source "drivers/char/hello/Kconfig"

增加上述代码之后,我们就可以使用Linux提供的menuconfig这个图形化工具来配置kernel了。在代码根目录下执行"make menuconfig",即可看到下面的界面:

  在该界面下,使用上下键可以导航不同的条目,使用回车进入条目的子条目,使用空格可以切换当前选中的条目的配置(即在ymn之间循环切换)。依次进入“Device Drivers --->” -> "Character devices --->" 就可以看到我们刚刚新增加的条目"<M> Device driver sample code, named hello",默认被配置为m,如下图所示。

2.3 编译kernel代码

  按照本博客《实验二》树莓派基本开发描述的方法编译完成后,就可以在代码目录的out/drivers/char/hello目录下生成hello.ko

三、模块的加载和卸载

3.1 加载内核模块

  内核模块运行于内核空间,因此需要sudo权限来执行。可以使用insmod来加载内核模块。根据上述对hello模块的解释,其作用是在加载的时候,将“Hello world!”输出到内核log中。

  树莓派进入Home界面后,使用U盘,将Ubuntu上编译好的hello.ko复制到树莓派上来。在终端中输入sudo insmod hello.ko,即可将上述编译好的hello.ko动态加载到内核中。

$ sudo insmod hello.ko

然后在终端中输入dmesg查看kernel log,在输出的log最后一行,我们就可以看到预期的现象 —— 模块加载的时候,打印出“Hello world!”。从而证明我们自己写的hello.ko模块已成功加载到内核中。

  另外,我们已可以使用lsmod命令来查看已加载到内核的所有模块:

$ lsmod
Module                  Size  Used by
hello                  16384  0             <-- 我们刚刚加载的内核模块
thunderbolt           167936  0
cmac                   16384  1
...

3.2 卸载内核模块

  卸载内核模块的操作环境同加载模块一样,在终端中执行sudo rmmod hello,即可完成内核模块的卸载。如下所示:

$ sudo rmmod hello

执行完毕之后,在终端中执行dmesg,查看kernel log:

[ 4011.853307] Goodbye world!

表示刚刚加载的hello.ko已成功卸载,并输出了我们预期的log —— 模块卸载的时候,打印出“Goodbye world!

  好的,目前为止,我们就学会了如何编写一个最简单的内核模块,并将其添加到kernel的代码中了。

【实验二】 树莓派基本开发

一、【实验目的】

  • 搭建起编译树莓派kernel代码的环境
  • 能够成功编译树莓派kernel代码
  • 能够烧写kernel镜像

二、【实验准备】

2.1 硬件准备

  • 一台安装有Ubuntu 18.04 PC机
  • 一个树莓派(本实验使用的是rapsberry pi 3B)
  • 一张至少8G以上的Micro SD卡
  • Micro SD卡读卡器
  • 一根Micro USB线
  • 树莓派电源(5V)
  • 一根HDMI线
  • 一个带有HDMI的显示器

2.2 软件准备

三、Ubuntu环境搭建

在我们的实验中,Ubuntu电脑主要用于代码的下载编译和烧写工作。在使用之前,需要安装一些必备的软件。安装命令如下:

sudo apt install git bc bison flex libssl-dev make
sudo apt install crossbuild-essential-armhf

四、树莓派内核代码下载

树莓派在github上提供了多个kernel版本,目前已经支持到最新的5.10版本。使用下面的命令默认下载的就是适用于树莓派的最新kernel版本:

git clone --depth=1 https://github.com/raspberrypi/linux

五、代码编译

代码位于Ubuntu电脑上,Ubuntu电脑是x86架构的计算机系统,而树莓派是基于Arm 32位的CPU,两者属于不同的架构。如果要想在Ubuntu上编译出能够在树莓派上运行的程序,需要使用交叉编译的方法。

实际操作中,怎么理解交叉编译呢?

在Ubuntu机器上,我们编译C程序一般是"gcc -o hello hello.c",这里gcc就是编译器。使用which命令查看gcc

which gcc
/usr/bin/gcc

而执行ls -l /usr/bin/gcc会发现/usr/bin/gcc是指向/usr/bin/gcc-7,进一步执行ls -l /usr/bin/gcc-7会发现/usr/bin/gcc-7最终指向/usr/bin/x86_64-linux-gnu-gcc-7。也就是说,Ubuntu上执行gcc实际上使用的是/usr/bin/x86_64-linux-gnu-gcc-7适用于x86_64架构的gcc编译器。
显然,直接用它编译出来的程序是无法在树莓派上运行的。那我们如果想在Ubuntu上编译适用于树莓派的程序,应该如何编译呢?其实只要安装一个交叉编译器就可以了。第三步的crossbuild-essential-armhf就是所需的交叉编译器。

编译树莓派3B的代码具体方法如下:

cd $RASBERRY_PI_SRC_ROOT              // 进入第四步下载的代码根目录
KERNEL=kernel7
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=./out bcm2709_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=./out zImage modules dtbs -j16

树莓派的代码是由Linux kernel延申而来,也使用了Makefile去管理代码,因此需要使用make命令编译代码。上述命令的各字段的含义是:

  • ARCH=arm表示编译运行于arm 32位平台的代码。
  • CROSS_COMPILE=arm-linux-gnueabihf-表示使用的编译器是arm-linux-gnueabihf-gcc
  • O=./out表示编译结果和临时文件都放到当前目录下的out目录下。
  • bcm2709_defconfig表示树莓派3B使用这个config文件
  • zImage modules dtbs表示从代码中需要编译出kernel镜像zImage,编译出所有配置的模块,编译出设备树
  • -j16表示编译时所使用的线程数

如果不出其他问题的话,执行完上述命令,就可以完成树莓派的kernel编译了。

六、kernel烧写

在使用raspberry pi imager烧写完sd卡之后,将这个sd卡插入ubuntu电脑执行以下命令:

$ sudo fdisk -l /dev/sdd
...
...

Device     Boot  Start      End  Sectors  Size Id Type
/dev/sdd1         8192   532479   524288  256M  c W95 FAT32 (LBA)
/dev/sdd2       532480 60776447 60243968 28.7G 83 Linux

$ df -Th /dev/sdd1 /dev/sdd2
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sdd1      vfat  253M   48M  205M  19% /media/user/boot
/dev/sdd2      ext4   29G  3.0G   25G  12% /media/user/rootfs

注意,每个人的sd卡分区名称(/dev/sdd1/dev/sdd2)可能不完全相同,请根据实际的sd卡大小,来判断那个分区是SD卡的块设备节点。比如有些人的sd卡插入Ubuntu之后,分区名字可能是/dev/sdb1/dev/sdb2

上述结果可以看出来,烧录器将sd卡分成了两个分区,第一个是/dev/sdd1,是vfat格式的文件系统,它是树莓派的boot分区,我们编译的kernel镜像就位于这个分区中。第二个/dev/sdd2,是ext4格式的文件系统,它是树莓派的rootfs分区,上层的一些应用程序和库就位于该分区内。

给树莓派刷入在第五步中编译出来的kernel镜像,实际上就是将编译结果复制到sd卡上的对应分区中,具体方法分为以下几个步骤。

6.1 挂载分区

将树莓派的sd卡插入Ubuntu机器,执行以下命令将sd卡的两个分区挂载到Ubuntu电脑上。

mkdir mnt
mkdir mnt/fat32                                 // 创建vfat分区的挂载目录
mkdir mnt/ext4                                  // 创建ext分区的挂载目录
sudo mount /dev/sdd1 mnt/fat32                  // 将/dev/sdd1分区挂载到mnt/fat32目录下
sudo mount /dev/sdb2 mnt/ext4                   // 将/dev/sdd2分区挂载到mnt/ext4目录下

6.2 复制kernel镜像

前面提到了,kernel编译的结果分为kernel image,modules和dtb。烧写镜像实际上就是把编译生成的这些文件复制到sd卡中。具体需要复制的文件和命令如下:

sudo cp /home/user/temp/mnt/fat32/$KERNEL.img /home/user/temp/mnt/fat32/$KERNEL-backup.img
sudo cp $RASBERRY_PI_SRC_ROOT/out/arch/arm/boot/zImage /home/user/temp/mnt/fat32/$KERNEL.img
sudo cp $RASBERRY_PI_SRC_ROOT/out/arch/arm/boot/dts/*.dtb /home/user/temp/mnt/fat32/
sudo cp $RASBERRY_PI_SRC_ROOT/out/arch/arm/boot/dts/overlays/*.dtb* /home/user/temp/mnt/fat32/overlays/
sudo cp $RASBERRY_PI_SRC_ROOT/out/arch/arm/boot/dts/overlays/README /home/user/temp/mnt/fat32/overlays/
sudo umount /home/user/temp/mnt/fat32
sudo umount /home/user/temp/mnt/ext4

注意。上述的$KERNEL对于树莓派3B来讲就是kernel7$RASBERRY_PI_SRC_ROOT需要替换成本地树莓派代码的真实路径。复制完成之后,就可以将sd卡插入树莓派直接开机了。

七、启动验证

树莓派启动后,打开终端,在终端中执行dmesg查看kernel log:

$ dmesg
...
Linux version 5.10.60-v7+ (user@hostname) (arm-linux-gnueabihf-gcc (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #1 SMP Sat Oct 23 12:06:52 CST 2021
...

如果烧录正常的话,上述log中的user应该是Ubuntu机器的用户名,hostname应该是Ubuntu的hostname。

八、参考资料

  1. Raspberry Pi Documentation

【实验一】练习树莓派基本使用

一、【实验目的】

  • 能够烧写树莓派,重刷系统
  • 掌握树莓派使用方法

二、【实验准备】

2.1 硬件准备

  • 一台安装有Ubuntu 18.04 PC机
  • 一个树莓派(本实验使用的是rapsberry pi 3B)
  • 一张至少8G以上的Micro SD卡
  • Micro SD卡读卡器
  • 一根Micro USB线
  • 树莓派电源(5V)
  • 一根HDMI线
  • 一个带有HDMI的显示器

2.2 软件准备

  • Ubuntu上安装 Raspberry Pi Imager

树莓派系统的安装有多种方式,这里使用树莓派推荐的 Raspberry Pi Imager来安装,点击这里从官网下载Download for Ubuntu for x86。下载完成后,执行以下命令安装该软件:

sudo dpkg -i imager_1.6.2_amd64.deb

三、【实验过程】

3.1 系统下载及烧写

将SD卡插入电脑,打开raspberry pi imager这个软件。如下图所示:

点击“CHOOSE OS”,选择“Raspberry Pi OS(32 bit)

点击“CHOOSE STORAGE”,软件能够自动识别sd卡,选择该sd卡

点击“WRITE

开始烧写

根据个人网络质量不同,烧写时间也不一样。正常的话,几分钟之后就可以完成烧写。出现下图表示烧写成功。

3.2 开机验证

  • 将SD卡插入树莓派
  • 通过HDMI线,将显示器连接到树莓派上
  • 通过micro USB线给树莓派 5V供电
    树莓派即可自动开机,如果能够进入Home界面,则表示树莓派烧录正常。

Linux设备驱动程序

  驱动程序是Linux中重要的一个部分,其大部分代码位于Linux源代码的"drivers"目录下,其代码行数占据了Linux总代码的一半左右。但是事实上,真正的驱动模型的核心代码总行数并不是大多。驱动程序那么多的代码,都是建立在驱动模型核心代码的基础上,为了支持各种各样不同的具体设备而编写的。而当掌握了驱动模型之后,编写驱动程序就像是搭积木一样,只需要在别人搭好的框架内填充一些硬件相关的代码而已。当然,这个过程会用到操作系统其他模块提供的接口,从这个角度来讲,实际上驱动程序是基于内核核心模块之上的程序。因此,有人也将驱动程序戏称为Linux kernel的“应用程序”。从其结构上来看,驱动程序是相对独立且简单的,正是由于这种特性,选择从驱动程序作为切入点去学习和理解内核相对来讲是比较容易的。因此,这个系列的文章就先从驱动程序谈起。

一、什么是驱动程序

  驱动程序是控制并使硬件能够正常工作的程序,同时,它提供了设备节点,以供应用程序通过访问这些节点控制设备或从设备中获取数据。。显然,理解硬件工作的原理是必须的,为了方便大家理解,这节我们从最简单的LED灯谈起,带大家认识学习一些硬件知识。

1.1 LED控制器

  根据最基本的物理知识,我们知道要让一个发光二极管(后面简称LED)正常工作(即点亮),只需要将合适的电源接在LED两端。如果要想控制LED的亮灭,可以在这个通路中添加一个物理开关。

  而在嵌入式系统中,为了能够使用程序来控制这个灯的亮灭,需要把物理开关替换成一个可以用软件控制的器件(如上图中红色虚线框所示),CPU执行不同的代码,即可以控制这个器件,从而使电路实现通断,最终实现控制开关的亮灭。那么,控制这个器件的代码就可以认为是这个LED灯的驱动程序,而这个器件就称之为LED控制器(注意,控制器件的代码适是执行在CPU上的)。
  在实际绘制原理图的过程中,由于电路系统的复杂性和电源的多样性,一般不会直接画出电源,而是使用Vcc表示电压源,GND表示接地,从而保证两者之前的器件存在电压。而所谓的LED控制器,这里实际上就是使用一个GPIO加上一个MOS管来实现——GPIO的高低电平控制MOS管的通断,从而控制LED灯两端的电压有无,最终达到控制LED灯的目的。

1.2 控制器(Controller)

  类似于LED控制器,如果想使用程序控制一个温度传感器,就也需要一个相应的控制器;如果想控制一个摄像头,就需要一个camera的控制器;如果想控制一个USB的鼠标就需要一个USB控制器。但是显然,如果我们针对每个器件都做一个控制器的话,是不太现实的,因为各种各样的器件实在是太多了。其实,很多器件的接口(即所使用的通信协议)都是一样的,没有必要针对每一个器件做一个控制器,而只针对使用相同接口的一类设备做一个通用的控制器,不管外面连接了什么设备,只要它们的接口(使用的协议)相同,就能够使用同一个控制器从设备中读取或向设备写入数据。
  举个例子,HDC1080是一款温湿度传感器,它使用了标准的i2c协议进行通信;而BMP280是一款大气压力传感器,同样使用了标准的i2c协议进行通信。因此,可以设计一款控制器能发送或者接收标准的i2c协议的数据。这样,就可以通过这个控制器和这两款芯片通信了,这种控制器就称为i2c控制器。类似的,我们可以为各种协议设计一个控制器,比如,用于SPI通信的SPI控制器,用于USB设备通信的USB控制器,用于PCI通信的PCI控制器等等,从而使用一个控制器就可以和一类设备通信。

1.3 SoC

  在现代计算机系统中,CPU是核心,而上述的这些控制器获取的数据最终都通过系统总线传输给了CPU来处理。所以,为了便于芯片使用,同时更加高效、快捷的传输数据,CPU生产商会把CPU和各种控制器集成到一个芯片内部,从而组成了一个计算机系统,这种集成电路被称作SoCSystem on a Chip)。其示意图如下:

但是在实际使用中情况往往比这个示意图复杂的多,SOC往往整合了各种各样的控制器和外设,甚至于还把其他类型的CPU也整合到了同一个芯片上,从而形成了功能复杂的计算机系统。下图是高通一款名为APQ8016E SGE SoC的框图,来源于高通官网

  图中标注有APQ8016E浅蓝色的框就是这款SOC,其内部根据各部分的作用不同,又分为以下几个部分:

  • Processors”就是指CPUs,在这个SoC中有两个CPU。它们分别是:
    • 负责处理主要应用的App Processor —— Arm Quad Cortex A53。这里需要注意的是,它是一个CPU,但是却有4个ARM核心(core)。这就是我们通常所说的“CPU是双核的,还是4核的”。
    • 另外一个是负责整个SoC电源管理的RPM处理器,它同样也是一颗ARM架构的CPU。
  • Memory support”指的是用于存储的控制器,比如内存控制器,emmc控制器。
  • Air interface”包括GNSS(导航相关的控制器)和WCN(无线设备相关的控制器,比如,wifi、bluetooth和FM)。
  • Multimedia”指的是多媒体相关的控制器,比如,camera sensor控制器,用于显示的MIPI控制器,Audio的控制器codec。
  • Connectivity”指的就是用于连接外部设备的控制器。比如,常见的i2c 控制器,SPI控制器,SD卡控制器,USB控制器等等。
  • GPIOs / PWR & GND”指的是GPIO控制器和芯片上的一些其他资源。
  • Internal functions”指的SoC内部的一些功能,比如clock发生器,温度传感器等等
  • Chipset & RFFE I/Fs”指的射频和基带相关的部分控制电路。

      综上所述,SoC主要就是CPU和一些控制器组成,而这些控制器的作用就是辅助CPU传输数据,控制外部设备。因此,他们都需要对应的驱动程序。

1.4 驱动程序

  在上一篇文章中,我们介绍了Linux的哲学之一——“一切都是文件”。意思是,对于Linux来讲,所有的IO(input/output)资源都是文件。前面也提到设备可以通过相应的控制器给CPU提供数据,显然设备也属于IO资源,也就是说,设备在Linux中也被抽象成了文件,那么是谁完成了这一动作?对,就是设备驱动程序。

  设备驱动程序不仅将设备抽象成了设备文件,隐藏了设备工作细节,还向应用程序提供了一组操作函数,使得用户能够通过标准的文件系统访问接口和这组操作函数来控制该设备。更为灵活的是驱动程序也可以像Linux的其他模块一样,在运行时动态的“插入”到内核,从而实现设备对应的功能。实际上,编写驱动程序就是在Linux模块的框架下,加入驱动硬件正常工作的代码,并使用统一的Linux接口对外提供操作访问硬件的接口

  综上,编写驱动程序首先要求对相应的硬件功能和操作有一定的了解,其次,要熟悉内核的很多知识,比如中断管理,并发和竞争,进程的调度。由于内核高度的模块化,因此,在编写驱动程序中使用到其他内核机制,我们甚至不需要去理解其具体实现,仅了解其用法就能写出完整的驱动程序。这也是我选择从驱动程序开始理解Linux内核的原因。在熟练使用这些机制的基础上,再去了解机制背后具体的实现方法及其框架就会有事半功倍的效果。

二、本系列的主要内容

  学习Linux驱动程序最核心的部分是掌握Linux设备模型,进而理解Linux如何管理所有的设备及其依赖关系。另一方面,设备驱动程序根据硬件工作的原理,抽象出了通用的接口供应用程序来访问设备数据或者控制设备,因此,对于编写驱动程序首要的任务是理解该硬件的工作原理,且不同的硬件工作原理也大不相同。比如,编写USB设备驱动需要对USB协议有一定的了解,而编写sensor的驱动,可能需要根据其接口不同,掌握不同串行通信协议,如i2c、spi等。
  这个系列的文章会从设备驱动程序的最基础框架——Linux的模块机制谈起,根据SoC组织设备的方式引入Device tree,从而讨论如何从Device tree中解析出系统中所有的device,这些device 又是如何被管理的。然后,我们会讨论driver是如何注册并管理的,device和driver如何bind并probe的。基于此编写一个最简单的字符设备驱动,从而对设备驱动程序建立一个直观的印象。然后,我们会研究并分析几个典型的设备驱动程序,进而抽象出设备驱动模型;有时间的话,我们会在这个基础上探究一些更为复杂的设备驱动,比如,usb设备驱动,display driver等。在这个过程中,我们还会学习一些驱动中常用的Linux机制,比如,为了处理并发和竞态而引入的各种锁、Linux的中断机制等知识。

  总之,Linux是一个复杂而精细的系统,研究的越透彻,愈能发现其魅力所在。你准备好了吗?让我们开始吧!

Linux设计哲学

说明:

  1. 所有代码均在Ubuntu 18.04(内核版本linux 5.4.0-52-generic)上编译运行通过
  2. 本文同步发表于官方博客——www.linuxrevealed.com

类Unix操作系统有一套由顶级开发人员积累起来并形成的、用于模块化软件开发的文化规范和哲学方法。这种理念强调构建简单、短小精悍、清晰、模块化并且可扩展的代码,这些特性使得代码可以由其编写者之外的人也能轻松的维护和重复使用。阅读和学习Linux系统的源码,不止是要学习其实现机制,方便开发或者解bug,还需要在阅读代码过程中学习其代码实现的原理及优点,从而指导我们自己也编写出优秀的代码。同时,掌握这些思想还可以帮助我们快速理解代码的框架,在阅读代码的过程中,起到事半功倍的效果。下面我们来认识几点Linux开发背后的哲学。

一、 只提供机制,不提供策略

  区分机制和策略是Unix设计背后隐含的最好思想之一。所谓的机制就是“程序能提供什么功能”,而所谓的策略就是“如何使用或者什么时候使用这些功能”。对于Linux操作系统来讲,它只提供了机制,并不提供策略。策略部分由运行于其上的应用程序来完成。

举个简单的例子来理解内核的"机制"和"策略"。比如,之前较早版本的Android手机上,都会有一个三色LED指示灯。当有未接来电、未读消息时,充电时,灯会有不同的状态。那么,对于LED驱动程序,它需要提供的机制可以包括:
1)LED灯亮或者灭(不同颜色值实际上对应的只是不同的LED设备而已);
2)LED灯以某个频率、某个颜色闪烁 (闪烁频率、颜色值由应用程序传入)。

对于不同场景下,LED闪亮的策略可以有如下设计:
1)当有未接来电时,调用红色LED驱动程序的接口,设置闪烁频率为每秒一次;
2)当有未读消息时,调用绿色LED驱动程序的接口,设置闪烁频率为每秒一次;
3)当电池处于充电状态,且电池电量还未达到100%时,调用黄色LED(实际上是同时控制红色LED和绿色LED)驱动程序的接口,设置其亮度,使其常亮;
4)当电池处于充电状态,且电池电量已达到100%时,调用绿色LED驱动程序的接口,设置其亮度,使其常亮;
注:上文中当... ...时,设置... ...这个动作就组成了一种策略,通俗来讲就是,在某个条件具备时,就执行某种特定的动作,这是由上层的应用程序来实现的。

结合上面的例子,驱动程序提供策略,赋予操作系统可以做某事的功能,而应用程序提供机制,调用操作系统提供的接口,在适当的时候去做某事。

  为什么要把机制和策略分开?而Linux只提供机制呢?

  这要从计算机说起,计算机是为用户服务的,它是用户实现某种需求或者完成某个任务的工具。而Linux是一个运行于计算机操作系统,操作系统的其中一个很重要的作用就是管理计算机的硬件资源,并提供接口给用户程序(应用程序)。而应用程序实际上就是代表用户来使用计算机的资源完成某项任务。那么,其中的从属关系自然就决定用户是主体,其意愿或想法需要通过应用程序调用操作系统接口来实现,因此,操作系统只需要提供机制,至于策略怎么实现,需要用户通过应用程序来决定。

  结合上面的例子,之前的项目中有一个需求:修改Charger的驱动程序,使得充电时,点亮黄色LED灯,充满电后点亮绿色LED灯。这实际上就在driver中增加了亮灯的策略——操作系统根据是否在充电决定了黄灯亮还是绿灯亮的策略。那么,假设有未读信息时,灯应该以每秒一次的频率闪烁绿色(应用程序控制的策略)。这样的话,当充满电,而且又来未读信息时,灯应该怎么设置呢?
  应用程序检测到有未读信息,会把灯设置成“绿灯每秒闪一次”,而驱动程序检测到此时已经充满电,根据它被赋予的策略——充电时,点亮黄色LED灯,充满电后点亮绿色LED灯,它会直接点亮绿灯。那么,此时灯就比较困惑了,到底是常亮呢?还是每秒闪一次呢?

因此,编写访问硬件的内核驱动程序代码时,不要在其中加入任何策略相关的代码。这样将机制与策略分离之后,可以保证不同的用户程序能够按照自己的需求使用驱动程序,而不出现不同用户之间策略的相互影响和干扰。

二、 分离和分层的思想

  代码实现的越是短小精悍,其复用程度越高,出问题的几率就越小。这种思想实际上就是要求代码中所有的子结构(函数或者模块)都需要“高内聚低耦合”。这种思想形成了Linux代码中分层和分离。
  分层思想实际上就是一个实现纵向的分为了不同的层次,每个层次使用下层提供的服务,并对这个层次的服务进程重新的组合和分发,再为其上层提供一个统一的接口,从而屏蔽掉下层的异构体。

  分层思想在设备驱动的代码中尤为常见。比如我们即将要看到的在设备驱动模型中,kobjectdeviceplatform_device就是这种关系。简单来讲,kobject抽象出了内核中一种对象,并提供了对这个对象操作的一些方法。这种对象是用来对包含它的对象做引用计数用的。而device在使用的时候偏偏就需要对其引用计数,以便在不被使用的时候被释放。所以,device结构就包含了kobject结构,并且device的大部分操作方法都调用了kobject的提供的API。类似的,platform_device是一种特殊的device,所以,platform_device包含了device结构,并且,platform_device相关的操作方法最终是由device的提供的API来实现的。

图1. 分层思想示意图

  当然,既然构造了kobject这种特殊结构,它并不只是用来给device结构使用的,内核中还有其他很多基础的结构其实都包含了kobject结构,比如,device_nodecdev也会直接包含kobject结构。类似的,device这种特殊结构,并不只是用于platform_device的,与platform_device类似的真实的设备像i2c_clientusb_device都存在对应的结构体,并直接包含device结构。

  分离思想就是把同一类设备的可复用的代码抽象出来,单独写成了一个驱动框架,具体设备相关的代码会再构成一个驱动,并配合上述驱动框架完成真正的设备驱动功能。比如,在子模块提供一个core driver,用于实现某一类设备的通用的控制方法,在这些控制方法中,真正需要控制某个设备时,会通过函数指针调用由不同的驱动程序实现的相应的设备的实际控制方法。这样就把对某类设备通用的操作的代码和具体设备特定的代码分离开来,从而最大程度的实现代码的复用,并且提高了代码的可扩展性。类似这种分层化的子模块在Linux中有很多,比如led driver,pwm driver等,后续我们分析相关代码的时候再来体会。
  模块的分离和分层的思想对于保证模块的低耦合和其高复用有很大作用,在后面博文中分析具体模块的代码时,我们会对其有深刻的体会的。
  
[^_^]:
设备驱动程序属于内核程序,因此一定要注意并发的处理。引起并发的原因可能有以下几种:
  1)Linux通常运行着多个并发的进程,而这些进程可能同时使用某个驱动程序,从而导致某个驱动程序的并发;
  2)中断处理程序的异步运行可能导致驱动程序的并发
  3)一些异步运行的软件抽象(比如内核定时器)也有可能导致并发
  4)SMP(对称多处理器)上,不同的CPU运行相同的程序也有可能导致并发
因此,对于驱动程序乃至内核中的其他模块在设计时,务必保证程序是可重入的,即它必须能够同时运行在多个上下文中。

三、 一切都是文件(Everything is a File)

  在Linux系统上,有一个概念——“一切都是文件”。意思指对于Linux系统,所有的IO资源都是文件,包括文件、目录、硬盘、设备,甚至一些虚拟的设备,比如管道等都被认为是文件。这么做的好处是读写这些资源都可用open()/close()/write()/read()等函数进行处理。屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户。虽然类型各不相同,但是对其提供的却是同一套API。更进一步,对文件的操作也可以跨文件系统执行。
  在Linux系统中有三类文件:普通文件目录文件特殊文件
  普通文件就是一般意义上我们所了解到的文件,比如打开计算机的某个文件夹之后,我们可以看到有保存日志的文本文件,存放照片的图像文件,存放烧写镜像的二进制文件等等,这些常规文件。
  目录文件就是存放上述普通文件的目录,在Linux中它也被看作一个文件,也可以被读,写等。
  最后一类,特殊文件,这类文件包括了代表硬盘之类的块设备文件,代表字符设备的字符设备文件,用于进程间通信管道的管道文件,网络通信使用到的socket文件等。后续的博客我们会一一接触到这些文件,并介绍这些文件的用法。

举个简单的例子,键盘在Linux系统中属于input类型的字符设备。那么,Linux也为连接到系统的键盘创建了一个对应的字符设备文件。执行ls -l命令可以看到当前系统中存在哪些input类的字符设备文件:

$ ls -l /dev/input
total 0
drwxr-xr-x 2 root root      60 3月  19 09:53 by-path
crw-rw---- 1 root input 13, 64 3月   8 15:34 event0
crw-rw---- 1 root input 13, 65 3月   8 15:34 event1
crw-rw----  1 root input 13, 66 3月   8 15:34 event2
crw-rw----  1 root input 13, 67 3月  20 17:51 event3
crw-rw----  1 root input 13, 63 3月   8 15:34 mice
crw-rw----  1 root input 13, 32 3月  20 17:51 mouse0
...

这里的“/dev/input/mouse0”就是鼠标对应的字符设备文件。上层应用程序如果需要监听鼠标事件(比如,鼠标位移、点击事件),直接读这个文件就可以得到了。执行sudo cat /dev/input/mouse0,然后,移动鼠标,则能够看到有数据输出。后面在介绍input子系统时,我们会详细分析这些数据。

  Linux的设计哲学其实还有很多,有兴趣的话,大家可以参考文末的参考阅读。

四、参考阅读

  1. 维基百科—— Everything is a file
  2. 维基百科—— Unix philosophy
  3. Linus Torvalds - "everything is a file descriptor or a process"
  4. Linux 设备驱动开发思想 —— 驱动分层与驱动分离
  5. 系统设计---分层,分级,分块

LinuxRevealed 引言

一、为什么要开设这个博客

  Linux是一个自由和开放源码的类UNIX操作系统。因此,它继承了Unix的很多优秀的特点,比如支持抢占式任务、多线程、虚拟内存、换页、动态链接和TCP/IP等。它诞生于1991年,当时其作者Linus出于兴趣和自己的需要编写了Linux,并将之开源到了Internet上,之后便吸引了很多开发者和黑客对其代码进行修改和完善,从而迅速风靡全球。如今,大到服务器,小到手机、手表等消费电子都采用了Linux内核。尤其对于嵌入式开发来讲,基于Linux的可能占了大半壁江山,因此,深入的学习Linux是不可避免也是非常有必要的。

  我本人基于Linux开发已有七八年时间,工作中经常会遇到各种各样的问题,但是都是遇到什么问题解决什么问题,从而使学习到的知识都是零碎的知识点,并不能形成完整的系统,时间长了,必然导致知识点可能会遗忘。解决工作中遇到的问题其实也是学习Linux的一种途径,但是,现实情况是为了快速解决问题,通过log快速定位到问题之后,再读一下某一部分代码,但是,并没有时间去系统的阅读某部分代码。这种浅尝辄止的做法,只会导致工作更加的被动,技术也得不到提升。为了能够系统学习Linux,我创建了这个博客,从零开始学习并整理Linux。

  这个博客名字叫做Linux revealed,中文名为"透视Linux",它来源于之前我之前看过一部纪录片Amarica revealed。在这个纪录片中详细的介绍了美国的农业、电力、制造和交通这几个方面,其讲述手法给我以深刻的印象。我在本博客中也会借鉴这种方法,和大家一起学习探讨Linux各模块提供的机制及其代码实现细节,同时,还会思考这些机制产生的背景及必要性。在本博客还会解析Linux代码中一些比较常用或者经典的全局变量函数,并将其分别整理出来,最终形成一个Linux学习使用的百科全书。

二、写作思路

  从结构上看,Linux和其他操作系统一样被划分为了几大模块:内存管理进程调度文件系统驱动程序网络管理。其实,各个模块之间的关系又是紧密结合的。比如,驱动程序在使用过程中,必然要调用内存管理的接口来向操作系统申请内存,而同时为了向用户提供接口,它又会提供一些sys节点,这些sys节点正是调用文件系统的接口来创建的。为了避免在跟踪代码的过程中偏离了主线,我会适当的控制对函数调用深度的跟踪。比如,阅读驱动程序的代码,跟踪到调用文件系统接口时,我会在简单介绍一下该接口的功能后,继续回到驱动程序的代码,往下阅读,而并不会深入该接口在文件系统相关代码的实现。当然,既然这些接口是跨模块使用的,那么其重要性可见一斑,所以,在学习到文件系统对应的代码的时候,我们自然会对这些函数展开详细的分析的。

  对代码的学习既要掌握其接口的使用方法,又要了解其背后运行的原理,还要清楚这部分代码在整个Linux系统中的作用。为了方便理解代码,对于每一个主题,我会先详细介绍其背景以及相关的理论知识,然后,再对该主题中的一些关键函数的代码逐行进行分析,从而归纳出函数的作用,以及在整个主题中的作用。最后,在通过实例来演示这些关键函数的用法,或者演绎该主题的运行原理和框架。这样看起来会有一些重复,但是,学习的过程本身是一个循环往复不断上升的过程。我这么写的目的就是在重复的过程中,对于一个主题能够全方位的理解,不仅掌握其用法,还需要理解其原理,更重要的是理解其在整个系统中的作用。

三、适合人群

  前面我也提到了,我写这个博客的目的,一方面是为了自己系统学习Linux,另外一方面是将学习的资料分类整理,这样也便于复习或者开发过程中使用。同时,我认为知识只有讲出来,才真正意味着自己确实理解了。所以,本博客的读者仅需要掌握C语言,同时对计算机原理相关的知识有一定了解即可。由于Linux本身是一个操作系统,如果读者有操作系统的基础最好。如果没有的话,也没关系,我会在文章中穿插介绍相关内容,并提供相关阅读材料。目的就是使不同水平的对Linux感兴趣的读者都能从本博客中收益。

四、学习环境搭建

  “工欲善其事,必先利其器”,在学习Linux之前,我们需要下载Linux代码,准备阅读代码的环境,还要有Linux代码编译运行的环境。为了更多人能够具有实践的环境,大多数实验都在QEMU环境中去完成。如果确实需要硬件的,我也会尽量选择树莓派这种成本比较低的硬件来做实验。根据每节主题不一样,所需环境不一样,具体的我会在每篇博客的正文之前去介绍。下面,我来简单介绍一下通用环境的搭建。

4.1 Linux环境

  学习Linux首先需要有一台运行Linux的机器,虚拟机也可以,但是最好是一台真正运行Linux的物理机器。Linux有很多发行版,这里推荐Ubuntu,网上相关的资料比较多,出现问题也更容易解决。Ubuntu的版本最好使用长期维护的版本,比如20.04,当然18.04也可以,再早的版本就不推荐了。

  

4.2 代码下载

  Linux源码的官方发布地址是kernel.org。由于Linux代码社区非常活跃,因此,为了便于维护和发布,Linux创建了很多分支,如下图所示。

  学习Linux代码一定要选取一个较新的Longterm版本,保证自己学习的内容不过时,且长期更新。这里我选择v5.4的版本。只需要点击版本后的tarball就可以下载对应版本代码的压缩包。
  另外,也可以从github上下载Linux源码,具体链接为:https://github.com/torvalds/linux,其下载命令为git clone https://github.com/torvalds/linux.git。需要注意的是这种方法下载的话,一般是同步的Linux主线上最新的代码,如果要想学习某个longterm版本的话,需要使用git tag命令,找到它对应的分支,然后执行git checkout -b xxx yyy(其中,xxx为新建的分支名字,yyy表示该longterm版本对应的tag名)切换到对应的longterm版本。

比如,我想找到v5.4 对应的tag,那么,进入Linux代码目录下,执行git tag之后,显示如下:

$ git tag
v2.6.11
v2.6.11-tree
v2.6.12
...
v5.3-rc8
v5.4      // 我选定的v5.4版本对应的tag名字
v5.4-rc1
...

然后,执行git checkout -b linux-v5.4 v5.4(其中,linux-v5.4是自己起的本地对应5.4版本的分支名, v5.4是v5.4对应的龙term版本的tag名)。那么当前文件夹下的代码即是v5.4对应的代码。

4.3 代码阅读环境

  阅读代码的工具很多,每个人可以根据自己的喜好设置。我本人比较习惯的是Opengrok + vim/ctags/cscope组合。具体来讲,对于不需要修改代码,单纯的阅读、搜索代码时,我习惯在自己搭建的Opengrok上读代码,这样可以很直观方便的搜索、跳转和复制代码。搭建Opengrok的方法可以参考Opengrok的安装及配置。对于需要修改并编译代码时,我更加习惯在Ubuntu系统上,使用ctags和cscope创建代码的tag和索引之后,使用vim打开源码修改和阅读。这两个软件的安装方法很简单,网上有很多相关的资料,大家可以自行搜索安装。方便使用的关键是这两个软件的插件和配置,有需要的同学可以通过公众号后台留言获取。

4.4 运行Linux系统

  前面提到过,为了能够让更多人能够以更低的成本学习Linux,我的大部分实验都在QEMU上运行。QEMU是一种虚拟机,它可以方便的模拟出多种硬件平台运行Linux代码。对于需要物理硬件平台的实验,比如在linux设备驱动的部分,我会选择树莓派。关于树莓派这里就不多做介绍了。大家可以自行在网上搜索查阅相关资料。

  目前,这个博客主要由我一个人来维护,加之工作原因,更新的频次可能不会太高,但是,我会尽量保证一周更新一篇博客。大家在阅读我的博客同时,有什么建议和意见,欢迎通过公众号或者博客留言和我交流。最后,如果你觉得我的博客写的还行,对你有一定的帮助,欢迎大家点赞、转发以使更多的朋友受益!谢谢!