操作系统第六次上机教程——Proc文件系统

@(学习资料)[OS, 文件系统, Proc伪文件系统]

理解Linux操作系统文件系统的概念

  • 岳友(yoyo9170@163.com)

上节课我们主要介绍了动态加载内核模块,并完成了相应的实验,本次实验主要讲解和考察的是Linux 文件系统,其中核心主要涉及到三个方面:Proc文件系统介绍、文件创建、文件读写、内核空间与用户空间交互。

Proc文件系统简介

Proc文件系统是一个虚拟文件系统,只存在于内存中,通过它可以查询、设置系统的运行情况及各种系统参数。
/proc目录下,每个文件的读写都可以绑定到一个内核函数上;当文件被读写时,被绑定的相应的内核函数就会被触发,由这些函数处理文件的输出与输入。
/proc目录下几乎包含了系统所有信息:

  • 可以通过 Proc文件系统收集系统信息;
  • 也可以用于程序调试;
  • 还可以动态的修改内核的数据结构,改变内核的运行状态

大家可以在自己的Linux系统里,进入到/proc目录下,会看到很多以数字命名的文件夹,这些一个个数字即对应着进程PID,根据上面提到的三点,不难理解这里主要收集系统运行进程的状态信息,具体对应参数和意义为:

文件/目录名 描述
Cmdline 该进程的命令行参数
Cwd 进程运行的当前路径的符号链接
Environ 该进程运行的环境变量
Exe 该进程相关的程序的符号链接
Fd 包含该进程使用的文件描述符
Maps 可执行文件或库文件的内存映像
Mem 该进程使用的内存
Root 该进程所有者的家(home)目录
Stat 进程状态
Statm 进程的内存状态
Status 用易读的方式表示的进程状态

当然/proc目录下还有一系列的文件,这些文件的作用
为:
cat /proc/cpuinfo 命令获得系统的CPU信息
cat /proc/meminfo 命令获得系统内存信息
cat /proc/loadvag 命令获得CPU平均负载
……(请大家自己查看)

1、核心数据结构

  • proc_dir_entry结构

代表各文件节点,管理操作系统从用户空间到内核空间对文件读写的驱动

1
2
3
4
5
6
7
8
9
10
struct proc_dir_entry { 
...
const char *name; //名字;
mode_t mode; //权限属性;
nlink_t nlink; //目录下子目录的数目;
read_proc_t *read_proc;//读操作函数
write_proc_t *write_proc;//写操作函数
struct file_operations *proc_fops; //文件操作函数;
...
}

2、创建文件

  • 头文件
    1
    #include <linux/proc_fs.h>
  • 创建文件系统
    1
    struct proc_dir_entry* create_proc_entry(const char* name, mode_t mode, struct proc_dir_entry* parent);

name:文件名
mode:文件类型和访问权限 parent:父目录,如果parent为根目录,则为NULL
返回值:成功,返回指向文件的proc_dir_entry结构指针.

  • 实例
    1
    foo_file = create_proc_entry(“foo”, 0644, example_dir);

3、删除文件

内核通过函数remove_proc_entry()删除proc文件

1
void remove_proc_entry(const char* name, struct proc_dir_entry* parent);

parent目录下移除名为name的文件 proc文件不能使用rmdirrm命令删除,只能使用上述函数 在删除文件之前,先释放proc_dir_entry结构中的data

4、创建目录

  • 创建一个目录

    1
    2
    3
    struct proc_dir_entry* proc_mkdir(const char* name, struct
    proc_dir_entry* parent)
    ;

    //目录名为name,父目录为parent
  • 创建符号连接

    1
    2
    3
    struct proc_dir_entry* proc_symlink(const char* name, struct
    proc_dir_entry* parent, const char* dest)
    ;

    //在parent目录下创建一个名字为name的符号连接,链接目标是dest

5、用户空间与内核空间数据传输

proc_dir_entry结构中的read_proc, write_proc是回调函数,分别定义了对文件的读写函数;

1
2
3
4
//实现读写文件接口
struct proc_dir_entry* foo_file;
foo_file->read_proc = proc_read_foobar;
foo_file->write_proc = proc_write_foobar;

6、读取文件

  • 读文件接口函数原型
    1
    2
    int read_func(char* page, char** start, off_t off, int count, int* eof,
    void* data)
    ;

其中,Page指针指向将写入数据的缓冲区 Start说明数据写在页面的位置 Count写入的字节数 Offset写入数据的位置相对于缓冲区的偏移量 Eof返回参数,当读到文件结尾时,该标志被置位 Data保存私有数据

7、写入文件

  • 写文件接口函数原型
    1
    2
    int write_func(struct file* file, const char* buffer, unsigned long count, void*
    data)
    ;

buffer数据缓冲区中读取count字节数的数据,写入到file文件
一般buffer指向用户空间缓冲区,应先调用copy_from_user将数据拷贝到内 核空间
一般省略file参数

代码示例

1、实现在/Proc目录下床间一个可读可写的my_procfile_1K文件

  • 编写 rw_proc.c 文件,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/proc_fs.h>
    #include <asm/uaccess.h> /* for copy_from_user */

    #define PROCFS_MAX_SIZE 1024
    #define PROCFS_NAME "my_procfile_1k"

    MODULE_LICENSE("GPL");
    static struct proc_dir_entry *Our_Proc_File;
    static char procfs_buffer[PROCFS_MAX_SIZE];
    static unsigned long procfs_buffer_size = 0;

    static int procfile_read(char *buffer,char **buffer_location, off_t offset, int buffer_length, int *eof,
    {
    int ret;
    void *data)

    printk(KERN_INFO "procfile_read (/proc/%s) called\n", PROCFS_NAME);

    if (offset > 0)
    {
    ret=0;
    }
    else{
    memcpy(buffer, procfs_buffer, procfs_buffer_size);
    ret = procfs_buffer_size;
    }
    return ret;
    }

    static int procfile_write(struct file *file, const char *buffer, unsigned long count, void *data)
    {

    procfs_buffer_size = count;
    if(procfs_buffer_size > PROCFS_MAX_SIZE )
    {
    procfs_buffer_size = PROCFS_MAX_SIZE;
    }
    if(copy_from_user(procfs_buffer, buffer, procfs_buffer_size) )
    {
    return EFAULT;
    }
    return procfs_buffer_size;
    }

    static int rw_proc_init(void)
    {

    Our_Proc_File = create_proc_entry(PROCFS_NAME, 0644, NULL);
    if (Our_Proc_File == NULL) {
    remove_proc_entry(PROCFS_NAME, &proc_root);
    printk(KERN_ALERT "Error: Could not initialize /proc/%s\n", PROCFS_NAME);
    return ENOMEM;
    }
    Our_Proc_File->read_proc = procfile_read;
    Our_Proc_File->write_proc = procfile_write;
    Our_Proc_File->owner = THIS_MODULE;
    Our_Proc_File->mode = S_IFREG | S_IRUGO;
    Our_Proc_File->uid = 0;
    Our_Proc_File->gid = 0;
    Our_Proc_File->size = 37;
    printk(KERN_INFO "/proc/%s created\n", PROCFS_NAME);
    return 0;
    }

    static void rw_proc_exit(void) {
    remove_proc_entry(PROCFS_NAME, &proc_root);
    printk(KERN_INFO "/proc/%s removed\n", PROCFS_NAME);
    }

    module_init(r w_proc_init);
    module_exit(rw_proc_exit);

    /*代码注释
    A> Our_Proc_File: /proc 文件系统下的每一个文件或目录项都对应一个proc_dir_entry 结构体实例。
    B> procfs_buffer: 这个缓冲区是我们 my_procfile_1k 的真实载体,本质上我们将来对my_procfile_1k 文件的读写都是对该缓冲区的读写。
    C> PROCFS_MAX_SIZE 和 procfs_buffer_size: 这两个值的关系就好比你有 4G(PROCFS_MAX_SIZE)的 U 盘,里面已用空间为 2.3G(procfs_buffer_size)一样。因此每次写入文件
    D> procfile_read:我们为文件 my_procfile_1k 实现的 read 函数。buffer 为写出内核空间的缓冲区(一个 page 大小),buffer_location 是用来写出大于一个 page 大 小的数据是用的参数。offset 是用来标记一个进程中对当前文件读写的偏移量。 buffer_len 是要读的大小,这里被忽略掉,直接返回了文件(buffer)的全部内容。eof 用来标识文件已经到达末尾。
    E> procfile_write:我们为文件 my_procfile_1k 实现的 write 函数。使用copy_from_user 函数的原因是 buffer 是一个指向用户地址空间的指针。
    F> create_proc_entry:用来创建一个 proc 文件系统的文件实例,创建目录实例的 函数为 proc_mkdir(char * dirname, struct proc_dir_entr * father_dir);两个函数的末尾参数置 NULL,表示父目录为/proc
    */

  • 编写 Makefile 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    KVER=$(shell uname -r) 
    KDIR=/lib/modules/$(KVER)/build
    PWD=$(shell pwd)

    obj-m += rw_proc.o
    default:
    make -C $(KDIR) M=$(PWD) modules
    clean:
    -rm *.ko
    -rm *.o
    -rm -f Mo*
    -rm *.mod.*
  • 编译模块 #make

  • 安装模块

    1
    #insmod rw_proc.ko
  • 测试/proc/my_procfile_1k 文件功能

    1
    2
    3
    4
    #cat /proc/my_procfile_1k
    #ls * > /proc/my_procfile_1k
    #cat /proc/my_procfile_1k
    #dmesg | tail -10 //查看系统日志里的打印信息
  • 卸载模块

    1
    rmmod

实验题目

1、(考查内核与用户空间的交互方式)在文档中给出的示例代码中procfile_read函数里面的offset>0分支里面添加printk打印信息到系统日志,并编写一个读my_procfile_1k的c语言程序。在程序路循环五次读取my_procfile_1k文件并打印读取的信息。(贴出代码和截图)查看内核日志信息。想想为什么。

2、(考查创建可读写的proc文件)在/proc目录下创建一个以task_info目录,并在里面创建一个pid文件和task_info文件:pid文件可读写,task_info文件只读。若pid=-1,则task_info文件的内容为当前系统所有进程的ID:应用程序名称的记录集合;若pid=>0task_info为进程pid的详细信息(自定义)。(请贴出代码并展示截图结果)
提示:
遍历所有进程可以用Linux/sched.h中的for_each_process宏实现:
struct tast_struct * task;
for_each_prcess(task){
}