欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux | proc虚拟文件系统

程序员文章站 2022-03-09 10:38:18
...

在安装新硬件到 Linux 系统之前,你会想要知道当前系统的资源配置状况。 Linux 将这类信息全集中在 /proc 文件系统下。/proc 目录下的文件都是 Linux 内核虚拟出来的,当你读取它们是,内核会实时提供文件内容。借由/proc,我们可得知系统的运作状态。例如,从/proc/interrupt、/proc/dma、/proc/ioports这几个文件,可分别查出系统的中断请求(IRQ)、DMA信道、I/O端口使用状况的状态。

介绍

/proc 文件系统是一个虚拟文件系统(没有任何一部分与 磁盘相关,只存在内存中,不占用外存空间),包含了一 些目录和虚拟文件

通过它可以在Linux内核空间和用户空间之间进行通信: 可以向用户呈现内核中的一些信息(用cat、more等命令 查看/proc文件中的信息),也可以用作一种从用户空间向内核发送信息的手段

LKM是用来展示 /proc 文件系统的一种简单方法,因为它可以动态地向 Linux 内核添加或删除代码

直观认识

Linux | proc虚拟文件系统
1
/proc 文件系统可以用于获取运行中的进程的信息。在 /proc 中有一些编号的子目录。每个编号的目录对应一个进程 id (PID)。这样,每一个运行中的进程 /proc 中都有一个用它的 PID 命名的目录。这些子目录中包含可以提供有关进程的状态和环境的重要细节信息的文件,它们是读取进程信息的接口。

2
还有一些文件的含义,如

apm # 高级电源管理信息 
bus # 总线配置信息(USB的配置也记录在此) 
cmdline # 内核命令行 
Cpuinfo # 关于Cpu信息 
Devices # 可以用到的设备(块设备/字符设备) 
Dma # 使用的DMA通道 
Filesystems # 支持的文件系统 
Interrupts # 中断的使用 
Ioports # I/O端口的使用 
Kcore # 内核核心印象 
Kmsg # 内核消息 
Ksyms # 内核符号表 
Loadavg # 负载均衡 
Locks # 内核锁 
Meminfo # 内存信息 
Misc # 杂项 
Modules # 加载模块列表(可以想成是驱动程序) 
Mounts # 加载的文件系统 
Partitions # 系统识别的分区表 
PCI # 在PCI总线上,每台设备的详细情况(可以使用lspci来查看) 
Rtc # 实时时钟 
Slabinfo Slab # 池信息 
stat # 全面统计状态表
Swaps # 对换空间的利用情况 
Version # 内核版本 
Uptime # 系统正常运行时间 

3
在/proc下还有三个很重要的目录:net,scsisys。sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi 目录不存在。

练习

内核中分别找出一处proc和seqfile的完整使用过程并记录下来

proc

proc_create函数开始,看看其中的实现

static inline struct proc_dir_entry *proc_create(
    const char *name, umode_t mode, struct proc_dir_entry *parent,
    const struct file_operations *proc_fops)
{
    return proc_create_data(name, mode, parent, proc_fops, NULL);
}

Linux | proc虚拟文件系统

函数返回一个proc_dir_entry。可以看到proc_create中直接调用了proc_create_data,而该函数主要完成2个功能

  1. 调用__proc_create完成具体proc_dir_entry的创建。
  2. 调用proc_register把entry注册进系统。
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
                    struct proc_dir_entry *parent,
                    const struct file_operations *proc_fops,
                    void *data)
{
    struct proc_dir_entry *pde;
    if ((mode & S_IFMT) == 0)
        mode |= S_IFREG;

    if (!S_ISREG(mode)) {
        WARN_ON(1);    /* use proc_mkdir() */
        return NULL;
    }

    if ((mode & S_IALLUGO) == 0)
        mode |= S_IRUGO;
    pde = __proc_create(&parent, name, mode, 1);
    if (!pde)
        goto out;
    pde->proc_fops = proc_fops;
    pde->data = data;
    if (proc_register(parent, pde) < 0)
        goto out_free;
    return pde;
out_free:
    kfree(pde);
out:
    return NULL;
}

先看proc_dir_entry的创建,这里通过__proc_create函数,其实该函数内部也很简单,就是为entry分配了空间,并对相关字段进行设置,主要包含name,namelen,mod,nlink等。创建好后,就设置操作函数proc_fops和data。然后就调用proc_register进行注册,

static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
    struct proc_dir_entry *tmp;
    int ret;

    ret = proc_alloc_inum(&dp->low_ino);
    if (ret)
        return ret;
     /*如果是 目录*/
    if (S_ISDIR(dp->mode)) {
        dp->proc_fops = &proc_dir_operations;
        dp->proc_iops = &proc_dir_inode_operations;
        dir->nlink++;
        /*如果是链接*/
    } else if (S_ISLNK(dp->mode)) {
        dp->proc_iops = &proc_link_inode_operations;
        /*如果是文件*/
    } else if (S_ISREG(dp->mode)) {
        BUG_ON(dp->proc_fops == NULL);
        dp->proc_iops = &proc_file_inode_operations;
    } else {
        WARN_ON(1);
        return -EINVAL;
    }

    spin_lock(&proc_subdir_lock);

    for (tmp = dir->subdir; tmp; tmp = tmp->next)
        if (strcmp(tmp->name, dp->name) == 0) {
            WARN(1, "proc_dir_entry '%s/%s' already registered\n",
                dir->name, dp->name);
            break;
        }
    /*子dir链接成链表,且子dir中含有父dir的指针*/
    dp->next = dir->subdir;
    dp->parent = dir;
    dir->subdir = dp;
    spin_unlock(&proc_subdir_lock);

    return 0;
}

函数首先分配一个inode number,然后根据entry的类型对其进行操作函数赋值,主要分为目录、链接、文件。这里我们只关注文件,文件的操作函数一般由用户自己定义,即上面我们设置的ops,这里仅仅是设置inode操作函数表,设置成了全局的proc_file_inode_operations,然后插入到父目录的子文件链表中,注意是头插法。基本结构如下,其中每个子节点都有指向父节点的指针。
(转自 忘存地址了)

seqfile

UNIX的世界里,文件是最普通的概念,所以用文件来作为内核和用户空间传递数据的接口也是再普通不过的事情,并且这样的接口对于shell也是相当友好的,方便管理员通过shell直接管理系统。由于伪文件系统proc文件系统在处理大数据结构(大于一页的数据)方面有比较大的局限性,使得在那种情况下进行编程特别别扭,很容易导致bug,所以序列文件接口被发明出来,它提供了更加友好的接口,以方便程序员。
Linux | proc虚拟文件系统

struct seq_file {
    char *buf;          //seq_file接口使用的缓存页指针
    size_t size;        //seq_file接口使用的缓存页大小
    size_t from;        //从seq_file中向用户态缓冲区拷贝时相对于buf的偏移地址
    size_t count;       //buf中可以拷贝到用户态的字符数目 
    loff_t index;       //start、next的处理的下标pos数值
    loff_t read_pos;    //当前已拷贝到用户态的数据量大小
    u64 version;
    struct mutex lock;      //针对此seq_file操作的互斥锁,所有seq_*的访问都会上锁
    const struct seq_operations *op; //操作实际底层数据的函数
    void *private;
};

在这个结构体中,几乎所有的成员都是由seq_file内部实现来处理的,程序员不用去关心,除非你要去研究seq_file的内部原理。对于这个结构体,程序员唯一要做的就是实现其中的const struct seq_operations *op。为使用 seq_file接口对于不同数据结构体进行访问,你必须创建一组简单的对象迭代操作函数。

seq_file内部机制使用这些接口函数访问底层的实际数据结构体,并不断沿数据序列向前,同时逐个输出序列里的数据到seq_file自建的缓存(大小为一页)中。也就是说seq_file内部机制帮你实现了对序列数据的读取和放入缓存的机制,你只需要实现底层的迭代函数接口就好了,因为这些是和你要访问的底层数据相关的,而seq_file属于上层抽象。这可能看起来有点复杂,大家看了下面的图就好理解了:

struct seq_operations {
    void * (*start) (struct seq_file *m, loff_t *pos);
    void (*stop) (struct seq_file *m, void *v);
    void * (*next) (struct seq_file *m, void *v, loff_t *pos);
    int (*show) (struct seq_file *m, void *v);
};
void * (*start) (struct seq_file *m, loff_t *pos);

start 方法会首先被调用,它的作用是在设置访问的起始点。
m:指向的是本seq_file的结构体,在正常情况下无需处理。
pos:是一个整型位置值,指示开始读取的位置。对于这个位置的意义完全取决于底层实现,不一定是字节为单位的位置,可能是一个元素的***。
返回值如果非NULL,则是一个指向迭代器实现的私有数据结构体指针。如果访问出错则返回NULL。

设置好了访问起始点,seq_file内部机制可能会使用show方法获取start返回值指向的结构体中的数据到内部缓存,并适时送往用户空间。

int (*show) (struct seq_file *m, void *v);

所以show方法就是负责将v指向元素中的数据输出到seq_file的内部缓存,但是其中必须借助seq_file提供的一些类似printf的接口函数:

int seq_printf(struct seq_file *sfile, const char *fmt, ...);
//专为 seq_file 实现的类似 printf 的函数;用于将数据常用的格式串和附加值参数.
//你必须将给 show 函数的 set_file 结构指针传递给它。如果seq_printf 返回-1,意味着缓存区已满,部分输出被丢弃。但是大部分时候都忽略了其返回值。

int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);
//类似 putc 和 puts 函数的功能,sfile参数和返回值与 seq_printf相同。
int seq_escape(struct seq_file *m, const char *s, const char *esc);
//这个函数类似 seq_puts ,但是它会将 s 中所有在 esc 中出现的字符以八进制格式输出到缓存。
//esc 的常用值是"\t\n\\", 它使内嵌的空格不会搞乱输出或迷惑 shell 脚本.

int seq_write(struct seq_file *seq, const void *data, size_t len) 
//直接将data指向的数据写入seq_file缓存,数据长度为len。用于非字符串数据。

int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);
//这个函数能够用来输出给定目录项关联的文件名,驱动极少使用。  

在show函数返回之后,seq_file机制可能需要移动到下一个数据元素,那就必须使用next方法

void * (*next) (struct seq_file *m, void *v, loff_t *pos);

在next实现中应当递增pos指向的值,但是具体递增的数量和迭代器的实现有关,不一定是1。而next的返回值如果非NULL,则是下一个需要输出到缓存的元素指针,否则表明已经输出结束,将会调用stop方法做清理。

void (*stop) (struct seq_file *m, void *v);

在stop实现中,参数m指向本seq_file的结构体,在正常情况下无需处理。而v是指向上一个next或start返回的元素指针。在需要做退出处理的时候才需要实现具体的功能。但是许多情况下可以直接返回。

在next和start的实现中可能需要对一个序列的函数进行遍历,而在内核中,对于一个序列数据结构体的实现一般是使用双向链表或者哈希链表,所有seq_file同时提供了一些对于内核双向链表和哈希链表的封装接口函数,方便程序员实现对于通过链表链接的结构体序列的操作。这些函数名一般是seq_list_*或者seq_hlist_*,这些函数的实现都在fs/seq_file.c中,有兴趣的朋友可以看看。我在后面的实验中依然使用内核通用的双向链表API。

在用户空间进行“读”、“写”

“读”

使用cat,读进程2的状态信息,比如PID 、PPID、State等
Linux | proc虚拟文件系统

“写”

使用echo,修改hostname
Linux | proc虚拟文件系统