kernel | 不想老是编译内核?sysfs和debugfs了解一下

编译内核是一件让大家都抗拒的事情,因为编译一次内核需要的时间成本比较漫长,而且如果每次代码的微小改动或者想要额外调用某一个函数执行某一个动作就要不断的编译内核的话,就相当于CPU大量的时间都用在了idle,开发效率将会是相当的低。

我们总是希望自己能够掌握自己想要调试的程序的一些状态从而来判断程序有没有正常的工作。在简单的场景下,我们仅需要使用printf大法,就可以打印出程序的轨迹,但是在复杂场景下,似乎没有那么好使,内核中的各个模块大家都一起向你打印出程序的轨迹,每时每刻都有洪水一般的日志输出,运行一小段时间后,光是日志文件都有十几个G,还得筛选出我们需要的,也是挺麻烦的一件事。

即使使用打印的方法,我们可以观察到程序运行的轨迹,但是我们依旧无法让程序去执行一些额外的操作,我们仅仅能知道程序运行到了这里,仅此而已。假如有一些复杂的情景,我们需要修改程序的一些状态量,从而使得代码逻辑切换,打印大法就无能为力了,诶,有一个办法是重新编译内核,然后继续开始无限循环......

有什么办法可以打破这种循环,使得我们能搭建用户态与内核态之间的桥梁,让内核模块的信息能够在用户态中交互?今天我们就来了解一下sysfs和debugfs,两个特殊的in-memory文件系统。

sysfs

简介

Linux 2.6 引入了sysfs文件系统,它把连接在系统上的设备和总线组织为一个分级的文件,它允许内核代码经由一个in-memory文件系统把信息报出到用户进程中,sysfs具有严格的目录组织形式,并构成了内核数据结构的内部组织的基础。在这种文件系统中产生的文件大多数是ASCII文件,通常每个文件有一个值,这些特征保证了被出报的信息的准确性且易被访问使得sysfs成为2.6内核的 最直观、最有用的特性之一。

sysfs是内核对象、属性及它们的相互关系的一种表现机制。一方面,在内核中,提供了sysfs相应功能的编程接口支持,另一方面,在用户态提供了对应的用户接口用于查看和操作所映射的内核对象条目。

内部 <--- sysfs ---> 外部
内核对象 <---> 目录
对象属性 <---> 常规文件
内核关系 <---> 软连接

用户接口

既然sysfs是目录、文件、软连接组成的集合而且它也是一个文件系统,那么就可以在shell中浏览和操作。sysfs的挂载点一般是/sys目录:

/sys$ tree -L 1
.
├── block 块IO子系统
├── bus   总线
├── class 按功能划分的设备类
├── dev   存放块设备和字符设备的主次设备号
│   ├── block 
│   └── char
├── devices  存放连接到系统的所有设备
├── firmware  
├── fs    文件系统
├── hypervisor
├── kernel debugfs挂载在这个目录之下
├── module 一些模块的交互
└── power

/sys路径下是一定数量的目录,这些目录代表了注册了sysfs的主要的子系统。所有目录项相当于sysfs中的中间节点,表示一个内核对象,所有的常规文件表示当前所在的内核对象所具备的某个属性。

属性根据它的读写权限不同,它可以是Read-Only、Write-Only或者Read-Write的。例如我们到/sys/bus/scsi目录下:

/sys/bus/scsi$ ll
total 0
drwxr-xr-x  4 root root    0 11月  3 01:02 ./
drwxr-xr-x 50 root root    0 11月  3 01:02 ../
drwxr-xr-x  2 root root    0 11月  3 01:02 devices/
drwxr-xr-x  4 root root    0 11月 29 19:54 drivers/
-rw-r--r--  1 root root 4096 11月 29 19:54 drivers_autoprobe
--w-------  1 root root 4096 11月 29 19:54 drivers_probe
--w-------  1 root root 4096 11月 29 19:54 uevent

我们可以发现,该路径下存在3个常规文件,是scsi总线的三个属性,这三个属性我们之前也有了解过,如果触发drivers_probe,则系统将会触发对所输入设备名的探测probe,但是它是Write-Only的,如果我们尝试去读取,会出现以下的报错:

$ cat drivers_probe 
cat: drivers_probe: Permission denied


在root用户可以执行,会触发对host1设备的探测:
# echo host1 > drivers_probe

同时,我们还可以发现,所有的属性结点的大小都是4096B,对应一页大小,实际上也是如此,操作系统为每一个属性固定分配一页空间。

但是,和常见的文件系统不同,用户接口是没有办法创建文件或者目录项的!这也很好理解,sysfs文件系统存在的目的是为了进行内核和用户的信息交换,你就算能创建一个文件或者目录项,操作系统又怎么知道你这个属性存在的意义是啥呢,或者说,用户态是无法去创建一个内核对象及其属性的。

即使是root用户,也会permission denied
/sys/bus/scsi: touch a
touch: cannot touch 'a': Permission denied

所以,sysfs所展示的都是内核通过内核编程接口,预先准备好的属性值。

内核编程接口

sysfs的目的是要能与内核空间进行信息的交互,人是活的内核是死的,一定只能让人去交换内核已经设置好的,愿意给你看的信息,这些信息就在内核编程中体现。

对于sysfs的内核常规文件而言,不能把它当作一个我们狭义所说的文件去处理,例如,对于RW文件,并不是说我们今天echo进去的内容就是明天cat出来的内容。在内核中,我们的一个echo和cat操作,将会触发一个已经预设好的函数。对于Read-Only的文件(属性),它将具有一个show方法,将在cat该属性的时候被调用;对于Write-Only的文件(属性),它将具有一个store方法,将在echo该属性的时候被调用;而对于Read-Write的文件(属性),它将同时具有以上的两个方法。

例如,我们不难找到,我们刚才例子中操作调用了对drivers_probe这个属性的store方法:

static ssize_t drivers_probe_store(struct bus_type *bus,
           const char *buf, size_t count)
{
  struct device *dev;
  int err = -EINVAL;


  dev = bus_find_device_by_name(bus, NULL, buf);
  if (!dev)
    return -ENODEV;
  if (bus_rescan_devices_helper(dev, NULL) == 0)
    err = count;
  put_device(dev);
  return err;
}

那内核是如何知道如果要访问这个属性,就应该调用这个函数的呢?sysfs提供了一系列的宏定义。

注册了一个bus的属性,名为drivers_probe
static BUS_ATTR_WO(drivers_probe);
宏定义展开为:
struct bus_attribute bus_attr_drivers_probe = 
{ .attr = { .name = "drivers_probe", .mode = 0200 }, 
.store = drivers_probe_store, }


mode = 0200 表示write-only
mode = 0444 表示read-only
mode = 0644 表示read-write


sysfs提供的宏定义,用于定义对应的结构体:
#define __ATTR(_name, _mode, _show, _store) {        \
  .attr = {.name = __stringify(_name),        \
     .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },    \
  .show  = _show,            \
  .store  = _store,            \
}


#define __ATTR_RO(_name) {            \
  .attr  = { .name = __stringify(_name), .mode = 0444 },    \
  .show  = _name##_show,            \
}


#define __ATTR_WO(_name) {            \
  .attr  = { .name = __stringify(_name), .mode = 0200 },    \
  .store  = _name##_store,          \
}


#define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store)

对应的结构体被定义好后,调用sysfs提供的方法,将它加入到对应的位置。

例如,drivers_probe属性,它通过bus_create_file方法加入sysfs:
static int add_probe_files(struct bus_type *bus)
{
  int retval;


  retval = bus_create_file(bus, &bus_attr_drivers_probe);
  if (retval)
    goto out;


  retval = bus_create_file(bus, &bus_attr_drivers_autoprobe);
  if (retval)
    bus_remove_file(bus, &bus_attr_drivers_probe);
out:
  return retval;
}
实际调用 sysfs_create_file 方法:
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr)
{
  int error;
  if (bus_get(bus)) {
    error = sysfs_create_file(&bus->p->subsys.kobj, &attr->attr);
    bus_put(bus);
  } else
    error = -EINVAL;
  return error;
}

sysfs中创建/删除文件、修改文件名、修改文件夹名等,都有对应的方法执行。但是具体的实现细节不是我们这里着重讨论的内容。同时,除了ACSII类型的属性文件,还有二进制类型的属性文件,它额外还支持了mmap方法等,也不是本文的重点内容。

总结

通过内核编程接口定义的属性及配套操作,在用户态通过用户接口,就可以调用到对应预设的方法,显示出属性的内容或者执行特定的功能。

debugfs

debugfs顾名思义则更加适应于调试场景它提供了一种在运行时获取和修改内核状态的机制,它与sysfs十分相似。它通常用于开发和调试过程中,以便开发人员能够查看和修改内核数据。而Sysfs主要用于设备管理和配置,有通用设备模型的影射的意义,它提供了设备的信息和状态,以及设备的控制接口,同时,sysfs限定了每个属性是一个值,debugfs则更加灵活,当然,对于相同的功能而言,一定要在sysfs中实现也是可以的,上线前把调试代码删了就好了。

在debugfs中开发可以更加的放飞自我,反正人家都明说了,我就是来debug的,开发者可以在debugfs里面放入更灵活、更多的信息,它可以做到对一个内核中的值进行修改、提供更多的方法、导出数据块、导出数组等。

启用debugfs

kernel | 不想老是编译内核?sysfs和debugfs了解一下_第1张图片

debugfs由CONFIG_DEBUG_FS内核编译选项控制,它是一个默认打开的编译选项,所以一般的发行版内核中已经支持了debugfs并且自动挂载:

debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)

利用debugfs导出基本数据类型变量

debugfs可以将内核中基本整数类型的变量导出为单个文件,在用户空间中可以直接对其读写(如使用cat、echo命令),只要权限允许即可。支持的类型有:u8, u16, u32, u64, size_t和 bool。其中bool类型在内核中要定义为u32类型,在用户空间中对应的文件内容则显示为Y或N。

static struct dentry *root_d = debugfs_create_dir("exam_debugfs", NULL); //在debugfs根目录下创建新目录exam_debugfs,然会新建目录的目录项指针
static u8 var8;
debugfs_create_u8("var-u8", 0664, root_d, &var8); //在exam_debugfs中创建变量var8对应的文件,名为var-u8,权限为0664
static u32 varbool;
debugfs_create_bool("var-bool", 0664, root_d, &varbool); //bool变量

debugfs挂载的路径需要root权限才可以访问:

21cdc72d18cefc82c2a4f164a7c87c21.png

例如,我们可以查看nvme0n1对应的tags使用情况,就可以通过以下方式:

/sys/kernel/debug/block/nvme0n1/hctx0# cat tags_bitmap 
00000000: 0000 0000 0000 0000 0000 0000 0000 0000
00000010: 0000 0000 0000 0000 0000 0000 0000 0000
00000020: 0000 0000 0000 0000 0000 0000 0000 0000
00000030: 0000 0000 0000 0000 0000 0000 0000 0000
00000040: 0000 0000 0000 0000 0000 0000 0000 0000
00000050: 0000 0000 0000 0000 0000 0000 0000 0000
00000060: 0000 0000 0000 0000 0000 0000 0000 0000
00000070: 0000 0000 0000 0000 0000 0000 0000 0000

翻阅内核代码:

static const struct blk_mq_debugfs_attr blk_mq_debugfs_hctx_attrs[] = {
  {"state", 0400, hctx_state_show},
  {"flags", 0400, hctx_flags_show},
  {"dispatch", 0400, .seq_ops = &hctx_dispatch_seq_ops},
  {"busy", 0400, hctx_busy_show},
  {"ctx_map", 0400, hctx_ctx_map_show},
  {"tags", 0400, hctx_tags_show},
  {"tags_bitmap", 0400, hctx_tags_bitmap_show},
  {"sched_tags", 0400, hctx_sched_tags_show},
  {"sched_tags_bitmap", 0400, hctx_sched_tags_bitmap_show},
  {"io_poll", 0600, hctx_io_poll_show, hctx_io_poll_write},
  {"dispatched", 0600, hctx_dispatched_show, hctx_dispatched_write},
  {"queued", 0600, hctx_queued_show, hctx_queued_write},
  {"run", 0600, hctx_run_show, hctx_run_write},
  {"active", 0400, hctx_active_show},
  {"dispatch_busy", 0400, hctx_dispatch_busy_show},
  {"type", 0400, hctx_type_show},
  {},
};


上方定义了如下的结构体数组:
struct blk_mq_debugfs_attr {
  const char *name;
  umode_t mode;
  int (*show)(void *, struct seq_file *);
  ssize_t (*write)(void *, const char __user *, size_t, loff_t *);
  /* Set either .show or .seq_ops. */
  const struct seq_operations *seq_ops;
};


于是我们刚才调用了tags_bitmap的show函数:
static int hctx_tags_bitmap_show(void *data, struct seq_file *m)
{
  struct blk_mq_hw_ctx *hctx = data;
  struct request_queue *q = hctx->queue;
  int res;


  res = mutex_lock_interruptible(&q->sysfs_lock);
  if (res)
    goto out;
  if (hctx->tags)
    sbitmap_bitmap_show(&hctx->tags->bitmap_tags->sb, m);
  mutex_unlock(&q->sysfs_lock);


out:
  return res;
}

blk-mq对每一个硬件队列都创建了一个硬件队列路径,并配置了上述相同的属性及方法:

void blk_mq_debugfs_register_hctx(struct request_queue *q,
          struct blk_mq_hw_ctx *hctx)
{
  struct blk_mq_ctx *ctx;
  char name[20];
  int i;


  snprintf(name, sizeof(name), "hctx%u", hctx->queue_num);
  hctx->debugfs_dir = debugfs_create_dir(name, q->debugfs_dir);


  debugfs_create_files(hctx->debugfs_dir, hctx, blk_mq_debugfs_hctx_attrs);


  hctx_for_each_ctx(hctx, ctx, i)
    blk_mq_debugfs_register_ctx(hctx, ctx);
}

同样的,debugfs也提供了若干方法,用于实现路径的管理、文件的创建/删除等等,相关的接口搜索引擎中都有,不是本文的重点。

总结

无论是使用sysfs还是debugfs,我们都可以实现内核和用户的信息交换,并可以向内核中注入函数,两者的学习成本实际上非常接近,但是由于sysfs有设备模型的影射含义且debugfs提供的功能更强劲,在我看来,我更倾向于开发者在开发调试过程中使用debugfs,如果能合理地使用debugfs(甚至完全没有必要去了解它们到底是如何工作的,会用就行),显而易见的一定能够提升内核代码开发调试效率。


参考:

https://www.cnblogs.com/pangblog/p/3283508.html

https://zhuanlan.zhihu.com/p/475213617

你可能感兴趣的:(服务器,数据库,linux,网络,运维)