本文简介

        由于字符设备和块设备都很好地体现了“一切都是文件”的设计思想,掌握Linux文件系统、设备文件系统的知识非常重要。

        首先,设备驱动最终通过操作系统的文件系统调用或C库函数(本质也基于系统调用)被访问。

        其次,驱动工程师在设备驱动中不可避免地会与设备文件系统打交道,如Linux2.4内核的devfs文件系统和Linux2.6内核的基于sysfs的udev文件系统。

        5.1节讲解了通过Linux API和C库函数在用户空间进行Linux文件操作的编程方法。

        5.2节分析了Linux文件系统的目录结构,简单介绍了Linux内核中文件系统的实现,并给出了文件系统和设备驱动的关系。

        5.3节和5.4节分别讲解了Linux2.4内核的devfs和Linux2.6内核所采用的udev设备文件系统,并分析了两者的区别。

 

 

5.1 Linux文件操作

一、文件操作的相关系统调用

        Linux的文件操作系统调用(在Windows编程领域,习惯称操作系统提供的接口为API)涉及创建、打开、读写和关闭文件。

1、创建

int creat(const char *filename, mode_t mode);

        参数 mode 指定新建文件的存取权限,它同 umask 一起决定文件的最终权限(mode&umask),其中 umask
代表了文件在创建时需要去掉的一些存取权限。 umask 可通过系统调用 umask()来改变,如下所示:

int umask(int newmask);

        该调用将umask设置为newmask,然后返回旧的umask,它只影响读、写和执行权限。

2、打开

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

        open()函数有两个形式,其中pathname是我们要打开的文件名(包含路径名称,默认是在当前路径下面),flags可以是如表5.1所示的一个值或者几个值的组合。

                                                                                 表5.1 文件打开标志

标志含义
O_RDONLY以只读的方式打开文件
O_WRONLY以只写的方式打开文件
O_RDWR以读写的方式打开文件
O_APPEND以追加的方式打开文件
O_CREAT创建一个文件
O_EXEC

如果使用了O_CREAT而且文件已经存在,

就会发生一个错误

O_NONBLOCK以非阻塞的方式打开一个文件
O_TRUNC如果文件已经存在,则删除文件的内容

        如果使用了O_CREAT标志,则使用的函数是int open(const char *pathname, int flags, mode_t mode),这个时候还要指定mode标志,用来表示文件的访问权限。mode可以是如表5.2所示值的组合。

                                                                                    表5.2 文件访问权限

标志含义
S_IRUSR用户可以读
S_IWUSR用户可以写
S_IXUSR用户可以执行
S_IRWXU用户可以读、写、执行
S_IRGRP组可以读
S_IWGRP组可以写
S_IXGRP组可以执行
S_IRWXG组可以读、写、执行
S_IROTH其他人可以读
S_IWOTH其他人可以写
S_IXOTH其他人可以执行
S_IRWXO其他人可以读、写、执行
S_ISUID设置用户的执行ID
S_ISGID设置组的执行ID

        除了可以通过上述宏进行“或”逻辑产生标志以外,我们还可以自己用数字来表示,Linux总共可以用5个数字来表示文件的各种权限:第一位表示设置用户ID,第二位表示设置组ID,第三位表示用户自己的权限位,第四位表示组的权限,第五位表示其他人的权限。每个数字可以取1(执行权限)、2(写权限)、4(读权限)、0(无)或者这些值的和。

        例如,如果要创建一个用户可读、可写、可执行,但是组没有权限,其他人可以读、可以执行的文件,并设置用户ID位。那么,应该使用的模式是1(设置用户组ID)、0(不设置组ID)、7(1+2+4,读、写、执行)、0(没有权限)、5(1+4,读、执行),即10705,如下所示:

open("test", O_CREAT, 10705);

        上述语句等价于:

open("test", O_CREAT, S_IRWXU | S_IROTH | S_IXOTH | S_ISUID);

        如果文件打开成功,open函数会返回一个文件描述符,以后对该文件的所有操作就可以通过对这个文件描述符进行操作来实现。

3、读写

        在文件打开后,我们才可以对文件进行读写,Linux系统中提供文件读写的系统调用是read、write函数,如下所示:

int read(int fd,const void *buf, size_t length);
int write(int fd,const void *buf, size_t length);

        其中参数buf为指向缓冲区的指针,length为缓冲区的大小(以字节为单位)。函数read()实现从文件描述符fd所指定的文件中读取length个字节到buf所指向的缓冲区中,返回值为实际读取的字节数。函数write实现把length个字节从buf指向的缓冲区中写到文件描述符fd所指向的文件中,返回值为实际写入的字节数。

        以O_CREAT为标志的open函数实际上实现了文件创建的功能,因此,下面的函数等同于create()函数:

int open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);

4、定位

        对于随机文件,我们可以随机地指定位置读写,使用如下函数进行定位:

int lseek(int fd, offset_t offset, int whence);

        lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的位置。参数whence可以使用如下值。

        SEEK_SET:相对文件开头;

        SEEK_CUT:相对文件读写指针的当前位置;

        SEEK_END:相对文件末尾。

        offset可取负值,例如下述调用可将文件指针相对当前位置向前移动5个字节。

lseek(fd, -5, SEEK_CUR);

        由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度:

lseek(fd, 0, SEEK_END);

5、关闭

        当操作完成以后,就要关闭文件了,只要调用close函数就可以,其中fd是要关闭的文件描述符。

int close(int fd);

        例程:编写一个程序,在当前目录下创建用户可读写文件“hello.txt”,在其中写入“Hello World”,关闭该文件。再次打开该文件,读取其中的内容并输出在屏幕上,如代码清单5.1所示。

 

                                                         代码清单5.1 Linux文件操作用户空间编程(使用系统调用)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#define LENGTH 100

void main(void){
    int fd, len;
    char str[LENGTH];

    /*创建并打开文件 */
    fd = open("hello.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); 

    if (fd){
        write(fd, "Hello World", strlen("Hello World")); /*写入字符串 */
        close(fd);
    }

    fd = open("hello.txt", O_RDWR);
    len = read(fd, str, LENGTH); /* 读取文件内容 */
    str[len] = '\0';
    printf("%s\n", str);
    close(fd);
}

        编译并运行,执行结果为输出“Hello World”。
 

二、C库函数的文件操作

        C库函数的文件操作实际上是独立于具体的操作系统平台的,不管是DOS、Windows、Linux还是在Vxworks中都是这些函数。

1、创建和打开

FILE *fopen(const char *path, const char *mode);

        fopen()实现打开指定文件filename,其中的mode为打开模式,C库函数中支持的打开模式如表5.3所示。

                                                                            表5.3 C库函数文件打开标志

标志含义
r、rb以只读方式打开
w、wb以只写方式打开。如果文件不存在,则创建该文件,否则文件被截断
a、ab以追加方式打开。如果文件不存在,则创建该文件
r+、r+b、rb+以读写方式打开
w+、w+b、wh+以读写方式打开。如果文件不存在,则创建该文件,否则文件被截断
a+、a+b、ab+以读和追加方式打开。如果文件不存在,则创建新文件

        其中b用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux系统不区分二进制文件和文本文件。

2、读写

        C库函数支持以字符、字符串等为单位,支持按照某种格式进行文件的读写,这一组函数为:

int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
char *fgets(char *s, int n, FILE *stream);
int fputs(const char *s, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);

        fread()实现从stream中读取n个字段,每个字段为size个字节,并将读取的字段放入ptr所指的字符数组中,返回实际已读取的字段数。在读取的字段数小于num时,可能是在函数调用时出现错误,也可能是读到文件的结尾,所以要通过调用feof()和ferror()来判断。

        write()实现从缓冲区ptr所指的数组中把n个字段写到stream中,每个字段长为size个字节,返回实际写入的字段数。

        另外,C库函数还提供了读写过程中的定位能力,这些函数包括:

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
int fseek(FILE *stream, long offset, int whence);

3、关闭

        利用C库函数关闭文件依然是很简单的操作,如下所示:

int fclose(FILE *stream);

        例程:上一个例程用C库函数来实现,如代码清单5.2所示。

 

                                          代码清单5.2 Linux文件操作用户空间编程范例(使用C库函数)

#include <stdio.h>
#define LENGTH 100
main(){
    FILE *fd;
    char str[LENGTH];

    fd = fopen("hello.txt", "W+");    /*创建并打开文件*/
    if(fd){
        fputs("Hello World", fd);    /*写入字符串*/
        fclose(fd);
    }

    fd = fopen("hello.txt","r");
    fgets(str, LENGTH, fd);    /*读取文件内容*/
    printf("%s\n", str);
    fclose(fd);
}

 

 

5.2 Linux文件系统

一、Linux文件系统目录结构

        进入Linux根目录(即“/”,Linux文件系统的入口,也是处于最高一级的目录),运行“ls -l”命令,可以看到Linux系统包含以下目录。

1、/bin

        包含基本命令,如ls、cp、mkdir等,这个目录中的文件都是可执行的。

2、/boot

        Linux系统的内核及引导系统程序所需要的文件,如vmlinuz、initrd.img文件都位于这个目录中。

3、/dev

        设备文件存储目录,应用程序通过这些文件的读写和控制就可以访问实际的设备。

4、/etc

        系统配置文件的所在地,一些服务器的配置文件也在这里,如用户账号及密码配置文件。

5、/home

        普通用户的家目录。

6、/lib

        库文件存放目录。

7、/lost+found

        在Ext2或Ext3文件系统中,当系统意外崩溃或机器意外关机时会产生一些碎片放在这里。

8、/mnt

        一般用于存放挂载储存设备的挂载目录,比如cdrom等目录,有时我们可以让系统开机自动挂载文件系统,把挂载点放在这里也是可以的。

9、/opt

        opt是可选的意思,有些软件包会被安装在这里,比如Fedora Core 5.0中的OpenOffice就是安装在这里,用户自己编译的软件包也可以安装在这个目录中。

10、/porc

        操作系统运行时,进程及内核信息(比如CPU、硬盘分区、内存信息等)存放在这里。/proc目录为伪文件系统proc的挂载目录,proc并不是真正的文件系统,它存放于内存之中。

11、/root

        linux超级权限用户root的家目录。

12、/sbin

        存放可执行文件,大多是涉及系统管理的命令,是超级权限用户root的可执行命令存放地,普通用户无权限执行这个目录下的命令。

13、/tmp

        有时用户运行程序的时候会产生临时文件,/tmp用来存放临时文件。

14、/usr

         这个是系统存放程序的目录,比如命令、帮助文件等,它包含很多文件和目录,Linux发行版提供的软件包大多被安装在这里。

15、/var

        var表示的是变化的意思,这个目录的内容经常变动,如/var的/var/log目录用来存放系统日志。

16、/sys

        Linux2.6内核所支持的sysfs文件系统被映射在此目录。Linux设备驱动模型中的总线、驱动和设备都可以在sysfs文件系统中找到对应的节点。当内核检测到在系统中出现了新的设备后,内核会在sysfs文件系统中为该新设备生成一项新的记录。

17、/initrd

        若在启动过程中使用了initrd映像作为临时根文件系统,则在执行完其上的/linuxrc挂接真正的根文件系统后,原来的初始RAM文件系统被映射到/initrd目录。

 

二、Linux文件系统与设备驱动

        图5.1所示为Linux系统中虚拟文件系统、磁盘文件(存放于Ramdisk、Flash、ROM、SD卡、U盘等文件系统中的文件也属于磁盘文件)及一般的设备文件与设备驱动程序之间的关系。

        应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是file_operations结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。

        由于字符设备的上层没有磁盘文件系统,所以字符设备的file_operations成员函数就直接由设备驱动提供了,file_operations正是字符设备驱动的核心。

 

                               

                                                                    图5.1 文件系统与设备驱动

 

         而对于块存储设备而言,ext2、fat、jffs2等文件系统中会实现针对VFS的file_operations成员函数,设备驱动层将看不到file_operations的存在。磁盘文件系统和设备驱动会将对磁盘上文件的访问最终转换成对磁盘上柱面和扇区的访问。

         在设备驱动程序的设计中,一般而言,会关心结构体file和inode这两个结构体。

1、file结构体

        文件结构体代表一个打开的文件(设备对应于设备文件),系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file的指针通常被命名为file或filp(即file pointer)。代码清单5.3给出了文件结构体的定义。

 

                                                                             代码订单5.3 文件结构体

struct file{
    union{
        struct list_head fu_list;
        struct rcu_head fu_rcuhead;
    } f_u;
    struct dentry *f_dentry;    /*与文件关联的目录入口(dentry)结构*/
    struct vfsmount *f_vfsmnt;
    struct file_operations *f_op;    /*和文件关联的操作*/
    atomic_t f_count;
    unsigned int f_flags;    /*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/
    node_t f_mode;    /*文件读写模式,FMODE_READ和FMODE_WRITE*/
    loff_t f_pos;    /*当前读写位置*/
    struct fown_struct f_owner;
    unsigned int f_uid, f_gid;
    struct file_ra_state f_ra;

    unsigned long f_version;
    void *f_security;

    /*tty驱动需要,其他驱动可能需要*/
    void *private_data;    /*文件私有数据*/
    
    #ifdef CONFIG_EPOLL
        struct list_head f_ep_links;
        spinlock_t f_ep_lock;
    #endif
    struct address_space *f_mapping;
};

        文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data在设备驱动中被广泛应用,大多被指向设备驱动自定义用于描述设备的结构体。

        驱动程序中经常使用如下类似的代码来检测用户打开文件的读写方式。

if(file->f_mode_t & FMODE_WRITE){
    //用户要求可写
}


if(file->mode_t & FMODE_READ){
    //用户要求可读
}

        下面的代码可用于判断以阻塞还是非阻塞方式打开文件。

if(file->f_flags & O_NONBLOCK)  //非阻塞
    pre_debug("open: non-blocking\n");

else                            //阻塞
    pre_debug("open: blocking\n");

2、inode结构体

        VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁,inode结构体的定义如代码清单5.4所示。

 

                                                                              代码清单5.4 inode结构体

struct inode{
    ...
    umode_t i_mode;    /*inode的权限*/
    uid_t i_uid;    /*inode拥有者的id*/
    gid_t i_gid;    /*inode所属的群组id*/
    dev_t i_rdev;    /*若是设备文件,此字段将记录设备的设备号*/
    loff_t i_size;    /*inode所代表的文件大小*/

    struct timespec i_atime;    /*inode最近一次的存取时间*/
    struct timespec i_mtime;    /*inode最近一次的修改时间*/
    struct timespec i_ctime;    /*inode的产生时间*/

    unsigned long i_blksize;    /*inode在做I/O时的区块大小*/
    unsigned long i_blocks;    /*inode所使用的block数,一个block为512byte*/

    struct block_device *i_bdev;
    struct cdev *i_cdev;
    ...
};

        对于表示设备文件的inode结构,i_rdev字段包含设备编号。Linux2.6设备编号分为主设备编号和次设备编号,前者为dev_t的高12位,后者为dev_t的低20位。下列操作用于从一个inode中获得主设备号和次设备号:

unsigned int imajor(struct inode *inode);
unsigned int iminor(struct inode *inode);

        主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,不同类的设备一般使用不同的主设备号(但是也不排除在同一主设备号下包含有一定差异的设备)。因为同一驱动可支持多个同类设备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。

 

 

5.3 devfs设备文件系统

        devfs(设备文件系统)是由Linux2.4内核引入的,引入时被许多工程师给予了高度评价,它的出现使得设备驱动程序能自主的管理它自己的设备文件。具体来说,devfs具有如下优点。

        可以通过程序在初始化时在/dev目录下创建设备文件,卸载设备时将它删除。

        设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。

        不再需要为设备驱动程序分配主设备号以及处理次设备号,在程序中可以直接给register_chrdev()传递0主设备号以动态获得可用的主设备号,并在devfs_register()中指定次设备号。

        驱动程序应调用下面这些函数来进行设备文件的创建和删除工作。

/*创建设备目录*/
devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const char *name, void *info);

/*创建设备文件*/
devfs_handle_t devfs_register(devfs_handle_t dir, const char *name, unsigned int flags, unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info);

/*撤销设备文件*/
void devfs_unregister(devfs_handle_t de);

        在Linux2.4的设备驱动编程中,分别在模块加载和卸载函数中创建和撤销设备文件是被普遍采用并值得大力推荐的好方法。代码清单5.5给出了一个使用devfs的例子。

 

                                                                              代码清单5.5 devfs的使用范例

static devfs_handle_t devfs_handle;
static int __init xxx_init(void){
	int ret;
	int i;
	/*在内核中注册设备*/
	ret = register_chrdev(xxx_MAJOR, DEVICE_NAME, &xxx_fops);
	if(ret < 0){
		printk(DEVICE_NAME "can't register major number\n");
		return ret;
	}
	/*创建设备文件*/
	devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT,
	XXX_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &xxx_fops, NULL);
	...
	printk(DEVICE_NAME "initialized\n");
	return 0;
}

static void __exit xxx_exit(void){
	devfs_unregister(devfs_handle);	/*撤销设备文件*/
	unregister_chrdev(XXX_MAJOR, DEVICE_NAME);	/*注销设备*/
}

module_init(xxx_init);
module_exit(xxx_exit);

 

 

5.4 udev设备文件系统

一、udev和devfs的区别

        尽管devfs有这样和那样的优点,但是,在Linux2.6内核中,devfs被认为是过时的方法,并最终被抛弃,udev取代了它。Linux VFS内核维护者AI Viro指出了udev取代devfs的几个原因:

        devfs所做的工作被确信可以在用户态来完成。

        一些bug相当长的时间内未被修复。

        devfs的维护者和作者停止了对代码的维护工作。

        udev完全在用户态工作,利用设备加入或移除时内核发送的热插拔事件来工作。在热插拔时,设备的详细信息会由内核输出到位于/sys的sysfs文件系统。udev的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用sysfs中的信息来进行创建设备文件节点等工作。

        由于udev根据系统中硬件设备的状态动态更新设备文件,进行设备文件的创建和删除,因此,在使用udev后,/dev目录下就会只包含系统中真正存在的设备了。

        devfs与udev的另一个显著区别在于:采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动,而udev则不能。这是因为udev的设计者认为Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候。udev的设计者认为devfs所提供的的打开/dev节点时自动加载驱动的功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生热插拔事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。

 

二、sysfs文件系统与Linux设备模型

1、sysfs文件系统

        Linux2.6内核引入了sysfs文件系统,sysfs被看成是与proc、devfs和devpty同类别的文件系统,该文件系统是一个虚拟的文件系统,它可以产生一个包括所有系统硬件的层级视图,与提供进程和状态信息的proc文件系统十分类似。

        sysfs把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性。sysfs的一个目的就是展示设备驱动模型中各组件的层级关系,其顶层目录包括block、device、bus、drivers、class、power和firmware。

        block目录包含所有的块设备,devices目录包含系统所有的设备并根据设备挂接的总线类型组织成层次结构,bus目录包含系统中所有的总线类型,drivers目录包含内核中所有已注册的设备驱动程序,class目录包含系统中的设备类型(如网卡设备、声卡设备、输入设备等)。

        在/sys/bus的子目录下,又会分出drivers和devices目录,而devices目录中的文件是对/sys/devices目录中文件的符号链接。同样地,/sys/class目录下包含许多对/sys/devices下文件的链接。如图5.2所示,这与设备、驱动、总线和类的现实状况是直接对应的,也正符合Linux2.6内核的设备模型。

 

                    

                                                                                  图5.2 Linux设备模型

 

        随着技术的不断进步,系统的拓扑结构越来越复杂,对智能电源管理、热插拔以及即插即用的支持要求也越来越高,Linux2.4内核已经难以满足这些要求。为适应这种形势的需要,Linux2.6内核开发了上述全新的设备、总线、类和驱动环环相扣的设备模型。

        大多数情况下,Linux2.6内核中的设备模型代码会处理好这些关系,内核中的总线和其他内核子系统会完成与设备模型的交互,这使得驱动工程师几乎不需要关心设备模型。但是,理解Linux设备模型的实现机制对驱动工程师仍然大有裨益,具体而言,内核将借助下文介绍的kobject、kset、subsystem、bus_type、device、device_driver、class、class_device、class_interface等重量级数据结构来完成设备模型的架构。

2、kobject内核对象

        kobject是Linux2.6引入的设备管理机制,在内核中由kobject结构体表示,这个数据结构使所有设备在底层都具有统一的接口。kobject提供了基本的对象管理能力,是构成Linux2.6设备模型的核心结构,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。kobject结构体的定义如代码清单5.6所示。

 

                                                                              代码清单5.6 kobject结构体

struct kobject{
    char *k_name;
    char name[KOBJ_NAME_LEN];    /*对象名称*/
    struct kref kref;    /*对象引用计数*/
    struct list_head entry;    /*用于挂接该kobject对象到kset链表*/
    struct kobject *parent;    /*指向父对象的指针*/
    struct kset *kset;    /*所属kset的指针*/
    struct kobj_type *ktype;    /*指向对象类型描述符的指针*/
    struct dentry *dentry;    /*sysfs文件系统中与该对象对应的文件节点入口*/
};

        内核通过kobjcet的kref成员实现对象引用计数管理,且提供两个函数kobject_get()、kobject_put()分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源都将被释放。

        kobject的ktype成员是一个指向kobj_type结构的指针,表示该对象的类型。如代码清单5.7所示,kobj_type数据结构包含3个成员:用于释放kobject占用资源的release()函数、指向sysfs操作的sysfs_ops指针和sysfs文件系统默认属性列表。

 

                                                                          代码清单5.7 kobject_type结构体

struct kobj_type{
    void (*release)(struct kobject *);    //release函数
    struct sysfs_ops *sysfs_ops;    //属性操作
    struct attribute **default_atrs;    //默认属性
};

        kobj_type结构体的sysfs_ops包括store()和show()两个成员函数,用于实现属性的读写,代码清单5.8给出了sysfs_ops结构体的定义。当从用户空间读取属性时,show()函数将被调用,该函数将指定属性值存入buffer中返回给用户,而store()函数用于存储用户通过buffer传入的属性值。和kobject不同的是,属性在sysfs中呈现为一个文件,而kobject则呈现为sysfs中的目录。

 

                                                                           代码清单5.8 sysfs_ops结构体

struct sysfs_ops{
    ssize_t (*show)(struct kobject *, struct attribute *, char *);
    ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
};

        Linux内核提供了一系列操作kobject的函数:

void kobject_init(struct kobject *kobj);

        该函数用于初始化kobject,它设置kobject引用计数为1,entry域指向自身,其所属kset引用计数加1.

int kobject_set_name(struct kobject *obj, const char *format, ...);

        该函数用于设置指定kobject的名称。

void kobject_cleanup(struct kobject *kobj);
void kobject_release(struct kref *kref);

        该函数用于清除kobject,当其引用计数为0时,释放对象占用的资源。

struct kobject *kobject_get(struct kobject *kobj);

        该函数用于将kobject对象的引用计数加1,同时返回该对象的指针。

void kobject_put(struct kobject *kobj);

        该函数用于将kobject对象的引用计数减1,如果引用计数降为0,则调用kobject_release()释放该kobject对象。

int kobject_add(struct kobject *kobj);

        该函数用于将kobject对象加入Linux设备层次,它会挂接该kobject对象到kset的list链中,增加父目录各级kobject的引用计数,在其parent指向的目录下创建文件节点,并启动该类型内核对象的hotplug函数。

int kobject_register(struct kobject *kobj);

        该函数用于注册kobject,它会先调用kobject_init()初始化kobj,再调用kobject_add()完成该内核对象的添加。

void kobject_del(struct kobject *kobj);

        这个函数是kobject_add()的反函数,它从Linux设备层次(hierarchy)中删除kobject对象。

void kobject_unregister(struct kobject *kobj);

        这个函数是kobject_register()的反函数,用于注销kobject。与kobject_register()相反,它首先调用kobject_del()从设备层次中删除该对象,再调用kobject_put()减少该对象的引用计数,如果引用计数降为0,则释放该kobject对象。

3、kset内核对象集合

        kobject通常通过kset组织成层次化的结构,kset是具有相同类型的kobject的集合,在内核中用kset数据结构表示,其定义如代码清单5.9所示。

 

                                                                                 代码清单5.9 kset结构体

struct kset{
    struct subsystem *subsys;    //所在的subsystem的指针
    struct kobj_type *ktype;    //指向该kset对象类型描述符的指针
    struct list_head list;    //用于连接该kset中所有kobject的链表头
    spinlock_t list_lock;
    struct kobject kobj;    //嵌入的kobject
    struct kset_uevent_ops *uevent_ops;    //事件操作集
};

        包含在kset中的所有kobject被组织成一个双向循环链表,list即是该链表的头。ktype成员指向一个kobj_type结构,被该kset中的所有kobject共享,表示这些对象的类型。kset数据结构还内嵌了一个kobject对象(kobj成员表示),所有属于这个kset的kobject对象的parent均指向这个内嵌的对象。此外,kset还依赖于kobj维护引用计数,kset的引用计数实际上就是内嵌的kobject对象的引用计数。

        与kobject相似,Linux提供了一系列函数操作kset。kset_init()完成指定kset的初始化,kset_get()和kset_put()分别增加和减少kset对象的引用计数,kset_add()和kset_del()函数分别实现将指定kset对象加入设备层次和从其中删除,kset_register()函数完成kset的注册,kset_unregister()函数则完成kset的注销。

        kobject被创建或删除时会产生事件(event),kobject所属的kset将有机会过滤时间或为用户空间添加信息。每个kset能支持一些特定的事件变量,在热插拔事件发生时,kset的成员函数可以设置一些事件变量,这些变量将被导出到用户空间。kset的uevent_ops成员是执行该kset事件操作集kset_event_ops的指针,kset_uevent_ops的定义如代码清单5.10所示。

 

                                                                 代码清单5.10 kset_uevent_ops结构体

struct kset_uevent_ops{
    int (*filter)(struct kset *kset, struct kobject *kobj);    //事件过滤
    const char *(*name)(struct kset *kset, struct kobject *kobj);
    int (*uevent)(struct kset *kset, struct kobjcet *kobj,
char **envp, int num_envp, char *buffer, int buffer_size);    //环境变量设置
};

        filter()函数用于过滤掉不需要导出到用户空间的事件,uevent()函数用于导出一些环境变量给用户的热插拔处理程序,各类设备导出的环境变量如下。

        PCI设备:ACTION("add"/"remove")、PCI_CLASS(16进制的PCI类、子类、接口,如c0310)、PCI_ID(Vendor:Device,如0123:4567)、PCI_SUBSYS_ID(子系统Vendor:子系统Device,如89ab:cdef)、PCI_SLOT_NAME(Bus:Slot.Func,如00:07.2)。

        USB设备:ACTION("add"/"remove")、DEVPATH(/sys/DEVPATH)、PRODUCT(idVendor/idProduct/bcdDevice,如46d/c281/108)、TYPE(bDeviceClass/bDeviceSubClass/bDeviceProtocol,如9/0/0)、INTERFACE(bInterfaceClass/bInterfaceSubClass/bInterfaceProtocol,如3/1/1)、如果内核配置了usbfs文件系统,还会导出DEVFS(USB驱动列表的位置,如/proc/bus/usb)和DEVICE(USB设备节点路径)。

        网络设备:ACTION("register"/"unregister")、INTERFACE(接口名,如“eth0”)。

        输入设备:ACTION("add"/"remove")、PRODUCT(idbus/idvendor/idproduct/idversion,如1/46d/c281/108)、NAME(设备名,如“ALCOR STRONG MAN KBD HUB”)、PHYS(设备物理地址ID,如usb-00:07.2-2.3/input0)、EV(来自evbit,如120002)、KEY(来自evbit,如e080ffdf 1dfffff ffffffff fffffe)、LED(来自ledbit,如7)。

        IEEE1394设备:ACTION("add"/"remove")、VENDOR_ID(24位的vendor id,如123456)、GUID(64位ID)、SPECIFIER_ID(24位协议标准拥有者ID)、VERSION(协议标准版本ID)。

        用户空间的热插拔脚本根据传入给它的参数(如PCI的参数为热插拔程序路径、"pci"和0)以及内核导出的环境变量采取相应的行动,如下面的脚本程序会在PRODUCT为“82d/100/0”的USB设备被插入时加载visor模块,在被拔出时卸载visor模块,如下所示。

if ["$1"="usb"]; then
    if["$PRODUCT"="82d/100/0"]; then
        if["$ACTION"="add"]; then
            /sbin/modprobe visor
        else
            /sbin/rmmod visor
        fi
    fi
fi

4、subsystem内核对象子系统

        subsystem是一系列kset的集合,它描述系统中某一类设备子系统,如block_subsys表示所有的块设备,对应于sysfs文件系统中的block目录。devices_subsys对应于sysfs中的devices目录,描述系统中所有的设备。subsystem由struct subsystem数据结构描述,其定义如代码清单5.11所示。

 

                                                                    代码清单5.11 subsystem结构体

struct subsystem{
    struct kset kset;    //内嵌的kset对象
    struct rw_semaphore rwsem;    //互斥访问信号量
};

        每个kset必须属于某个subsystem,通过设置kset结构中的subsys域指向指定的subsystem可以将一个kset加入到该subsystem。所有挂接到同一个subsystem的kset共享同一个rwsem信号量,用于同步访问kset中的链表。

        内核也提供了一组类似于kobject和kset的操作subsystem的操作,如下所示:

void subsystem_init(struct subsystem *subsys);
int subsystem_register(struct subsystem *subsys);
void subsystem_unregister(struct subsystem *subsys);
struct subsystem *subsys_get(struct subsystem *subsys);
void subsys_put(struct subsystem *subsys);

5、Linux设备模型组件

        系统中的任一设备在设备模型中都由一个device对象描述,其对应的数据结构struct device定义如代码清单5.12所示。

 

                                                                代码清单5.12 device结构体

struct device{
    struct klist klist_children;    //设备列表中的孩子列表
    struct klist_node knode_parent;    //兄弟节点
    struct klist_node knode_driver;    //驱动节点
    struct klist_node knode_bus;    //兄弟节点
    struct device *parent;    //指向父设备

    struct kobject kobj;    //内嵌一个kobject对象
    char bus_id[BUS_ID_SIZE];    //总线上的位置
    struct device_attribute uevent_attr;

    struct semaphore sem;
    
    struct bus_type *bus;    //总线
    struct device_driver *driver;    //使用的驱动
    void *driver_data;    //驱动私有数据
    void *platform_data;    //平台特定的数据
    void *firmware_data;    //固件特定的数据(如ACPI、BIOS数据)
    struct dev_pm_info power;

    u64 *dma_mask;    //dma掩码
    u64 coherent_dma_mask;
    struct list_head dma_pools;    //DMA缓冲池
    stuct dma_coherent_mem *dma_mem;

    void (*release)(struct device *dev);    //释放设备方法
};

        device结构体用于描述设备相关的信息、设备之间的层次关系,以及设备与总线、驱动的关系。

        内核提供了相应的函数用于操作device对象。其中device_register()函数将一个新的device对象插入设备模型,并自动在/sys/devices下创建一个对应的目录。device_unregister()完成相反的操作,注销设备对象。get_device()和put_device()分别增加和减少设备对象的引用计数。通常device结构体不单独使用,而是包含在更大的结构体中,比如描述PCI设备的struct pci_dev,其中的dev域就是一个device对象。

        系统中的每个驱动程序由一个device_driver对象描述,对应的数据结构的定义如代码清单5.13所示。

 

                                                          代码清单5.13 device_driver结构体

struct device_driver{
    const char *name;    //设备驱动程序的名称
    struct bus_type *bus;    //总线
    
    struct completion unloaded;
    struct kobject kobj;    //内嵌的kobject对象
    struct klist klist_devices;
    struct klist_node knode_bus;

    struct module *owner;

    int (*probe)(struct device *dev);    //指向设备探测函数
    int (*remove)(struct device *dev);    //指向设备移除函数
    void (*shutdown)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
};

        与device结构体类似,device_driver对象依靠内嵌的kobject对象实现引用计数管理和层次结构组织。内核提供类似的函数用于操作device_driver对象。如get_driver()增加引用计数,driver_register()用于向设备模型插入新的driver对象,同时在sysfs文件系统中创建对应的目录。device_driver()结构体还包括几个函数,用于处理探测、移除和电源管理事件。

        系统中总线由struct bus_type描述,其定义如代码清单5.14所示。

 

                                                                    代码清单5.14 bus_type结构体

struct bus_type{
    const char *name;    //总线类型的名称

    struct subsystem subsys;    //与该总线相关的subsystem
    struct kset drivers;    //所有与该总线相关的驱动程序集合
    struct kset devices;    //所有挂接在该总线上的设备集合
    struct klist klist_devices;
    struct klist klist_drivers;

    struct bus_attribute *bus_attrs;    //总线属性
    struct device_attribute *dev_attrs;    //设备属性
    struct driver_attribute *drv_attrs;    //驱动程序属性

    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, char **envp, int num_envp, char *buffer,
        int buffer_size);    //事件
    int (*probe)(struct device *dev);
    int (*remove)(strut device *dev);
    void (*shutdown)(struct device *dev);
    int (*suspend)(struct device *dev, pm_massage_t state);
    int (*resume)(struct device *dev);
};

        每个bus_type对象都内嵌一个subsystem对象,bus_subsys对象管理系统中所有总线类型的subsystem对象。每个bus_type对象都对应/sys/bus目录下的一个子目录,如PCI总线类型对应于/sys/bus/pci。在每个这样的目录下都存在两个子目录:devices和drivers(分别对应于bus_type结构中的devices和drivers域)。其中devices子目录描述连接在该总线上的所有设备,而drivers目录则描述与该总线关联的所有驱动程序。与device_driver对象相似,bus_type结构还包含几个和热插拔、即插即用和电源管理事件相关的函数。

        系统中的设备类由struct class描述,表示某一类设备。所有的class对象都属于class_subsys子系统,对应于sysfs文件系统中的/sys/class目录。代码清单5.15给出了class结构体的定义。

 

                                                                                代码清单5.15 class结构体

struct class{
    const char *name;    //类名
    struct module *owner;    

    struct subsystem subsys;    //对应的subsystem
    struct list_head children;    //class_device链表
    struct list_head interfaces;    //class_interface链表
    struct semaphore sem;    //children和interfaces链表锁

    struct class_attribute *class_attrs;    //类属性
    struct class_device_attribute *class_dev_attrs;    //类设备属性

    int (*uevent)(struct class_device *dev, char **envp, 
        int num_envp, char *buffer, int buffer_size);    //事件

    void (*release)(struct class_device *dev);
    void (*class_release)(struct class *class);
};

        每个class对象包括一个class_device链表,每个class_device对象表示一个逻辑设备,并通过struct class_device中的dev成员(一个指向struct device的指针)关联一个物理设备。这样,一个逻辑设备总是对应于一个物理设备,但是一个物理设备却可能对应于多个逻辑设备,代码清单5.16给出了class_device的定义。此外,class结构中还包括用于处理热插拔、即插即用和电源管理事件的函数,这与bus_type对象相似。

 

                                                              代码清单5.16 class_device结构体 

struct class_device{
    struct list_head node;

    struct kobject kobj;    //内嵌的kobject对象
    struct class *class;    //所属的类
    dev_t devt;    //dev_t
    struct class_device_attribute *devt_attr;    
    struct class_device_attribute uevent_attr;
    struct device *dev;    //如果存在,创建到/sys/devices相应入口的符号链接
    void *class_data;    //私有数据
    struct class_device *parent;    //父设备

    void (*release)(struct class_device *dev);
    int(*uevent)(struct class_device *dev, char **envp, int num_evnp,
        char *buffer, int buffer_size);
    char class_id[BUS_ID_SIZE];    //类标识
};

        下面两个函数用于注册和注销class。

int class_register(struct class *cls);
void class_unregister(struct class *cls);

        下面两个函数用于注册和注销class_device。

int class_device_register(struct class_device *class_dev);
void class_device_unregister(struct class_device *class_dev);

        除了class、class_device结构体外,还存在一个class_interface结构体,当设备加入或离开类时,将引发class_interface中的成员函数被调用,class_interface的定义如代码清单5.17所示。

 

                                                                 代码清单5.17 class_interface结构体

struct class_interface{
    struct list_head node;
    struct class *class;    //对应的class
    int (*add)(struct class_device *, struct class_interface *);    //设备加入时触发
    void (*remove)(struct class_device *, struct class_interface *);    //设备移除时触发
};

        下面两个函数用于注册和注销class_interface。

int class_interface_register(struct class_interface *class_intf);
void class_interface_unregister(struct class_interface *class_intf);

6、属性

        在bus、device、driver和class层级上都分别定义了其属性结构体,包括bus_attribute、driver_attribute、class_attribute、class_device_attribute,这几个结构体的定义在本质上是完全相同的,如代码清单5.18所示。

 

                    代码清单5.18 bus_attribute/driver_attribute/class_attribute/class_device_attribute结构体

/*总线属性*/
struct bus_attribute{
    struct attribute attr;
    ssize_t (*show)(struct bus_type *, char *buf);
    ssize_t (*store)(struct bus_type *, const char *buf, size_t count);
};

/*驱动属性*/
struct driver_attribute{
    struct attribute attr;
    ssize_t (*show)(struct device_driver *, char *buf);
    ssize_t (*store)(struct device_driver *, const char *buf, size_t count);
};


/*类属性*/
struct class_attribute{
    struct attribute attr;
    ssize_t (*show)(struct class *, char *buf);
    ssize_t (*store)(struct class *, const char *buf, size_t count);
};

/*类设备属性*/
struct class_device_attribute{
    struct attribute attr;
    ssize_t (*show)(struct class_device *, char *buf);
    ssize_t (*store)(struct class_device *, const char *buf, size_t count);
};

        下面一组宏分别用于创建和初始化bus_attribute、driver_attribute、class_attribute、class_device_attribute。

BUS_ATTR(_name, _mode, _show, _store)
DRIVER_ATTR(_name, _mode, _show,_store)
CLASS_ATTR(_name, _mode, _show, _store)
CLASS_DEVECE_ATTR(_name, _mode, _show, _store)

        下面一组函数分别用于添加和删除bus、driver、class、class_device属性。

int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);

int device_create_file(struct device *dev, struct device_attribute *attr);
void device_remove_file(struct device *dev, struct device_attribute *attr);

int class_create_file(struct class *cls, const struct class_attribute *attr);
void class_remove_file(struct class *cls, const struct class_attribute *attr);

int class_device_create_file(struct class_device *class_dev, 
        const struct class_device_attribute *attr);
void class_device_remove_file(struct class_device *class_dev, 
        const struct class_device_attribute *attr);

        xxx_create_file()函数中会调用sysfs_create_file(),而xxx_remove_file()函数中会调用sysfs_remove_file()函数,xxx_create_file()会创建对应的sysfs文件节点,而xxx_remove_file()会删除对应的xxx文件节点。

 

三、udev的组成

        udev的设计目标如下。

        在用户空间执行。

        动态建立/删除设备文件。

        允许每个人都不用关心主/次设备号。

        提供LSB标准名称。

        如果需要,可提供固定的名称。

        为了提供这些功能,udev以3个分割的子计划发展:namedev、libsysfs和udev。namedev为设备命名子系统,libsysfs提供访问sysfs文件系统从中获取信息的标准接口,udev提供/dev设备节点文件的动态创建和删除策略。udev程序承担与namedev和libsysfs库交互的任务,当/sbin/hotplug程序被内核调用时,udev将被运行。udev的工作过程如下。

        (1)当内核检测到在系统中出现了新设备后,内核会在sysfs文件系统中为该新设备生成新的记录并导出一些设备特定的信息及所发生的事件。

        (2)udev获取内核导出的信息,它调用namedev决定应该给设备指定的名称,如果是新插入设备,udev将调用libsysfs决定应该为该设备的设备文件指定的主/次设备号,并用分析获得的设备名称和主/次设备号创建/dev中的设备文件;如果是设备移除,则之前已经被创建的/dev文件将被删除。

        在namedev中使用5个步骤来决定指定设备的命名。

        (1)标签(label)/序号(serial):这一步检查设备是否有唯一的识别记号,例如USB设备有唯一的USB序号,SCSI有唯一的UUID。如果namedev找到与这种唯一编号相对应的规则,它将使用该规则提供的名称。

        (2)设备总线号:这一步会检查总线设备编号,对于不可热插拔的环境,这一步足以辨识设备。例如,PCI总线编号在系统的使用期间内很少变更。如果namedev找到相对应的规则,规则中的名称就会被使用。

        (3)总线上的拓扑:当设备在总线上的位置匹配用户指定的规则时,就会使用该规则指定的名称。

        (4)替换名称:当内核提供的名称匹配指定的替代字符串时,就会使用替代字符串指定的名称。

        (5)内核提供的名称:如果以前的几个步骤都没有被提供,缺省的内核将被指定给该设备。

        代码清单5.19给出了一个namedev命名规则的例子,第2、4行定义的是符合第一步的规则,第6、8行定义的是符合第二步的规则,第11、14行定义的是符合第三步的规则,第16行定义的是符合第4步的规则。

 

                                                                         代码清单5.19 namedev命名规则

# USB Epson printer to be called lp_epson
LABEL, BUS="usb", serial="HXOLL0012202323480", NAME="lp_epson"
# USB HP printer to be called lp_hp,
LABEL, BUS="usb", serial="W09090207101241330", NAME="lp_hp"
# sound card with PCI bus id 00:0b.0 to be the first sound card
NUMBER, BUS="pci", id="00:0b.0", NAME="dsp"
#sound card with PCI bus id 00:07.1 to be the second sound card
NUMBER, BUS="pci", id="00:07.1", NAME="dsp1"
# USB mouse plugged into the third port of the first hub to be
# called mouse0
TOPOLOGY, BUS="usb", place="1.3", NAME="mouse0"
# USB tablet plugged into the second port of the second hub to be
# called mouse1
TOPOLOGY, BUS="usb", place="2.2", NAME="mouse1"
# ttyUSB1 should always be called visor
REPLACE, KERNEL="ttyUSB1", NAME="visor"

 

四、udev规则文件

        udev规则文件以行为单位,以“#”开头的行代表注释行。其余的每一行代表一个规则。每个规则分成一个或多个匹配和赋值部分。匹配部分用匹配专用的关键字来表示,相应的赋值部分用赋值专用的关键字来表示。

        匹配关键字包括:ACTION(用于匹配行为)、KERNEL(用于匹配内核设备名)、BUS(用于匹配总线类型)、SYSFS(用于匹配从sysfs得到的信息,比如label、vendor、USB序列号)、SUBSYSTEM(匹配子系统名)等,赋值关键字包括:NAME(创建的设备文件名)、SYMLINK(符号创建链接名)、OWNER(设置设备的所有者)、GROUP(设置设备的组)、IMPORT(调用外部程序)等。

        例如如下规则:

SUBSYSTEM=="net", ACTION=="add", SYSFS{address}=="00:0d:87:f6:59:f3", IMPORT="/sbin/rename_netiface %k eth0"

        其中的匹配部分有3项,分别为SUBSYSTEM、ACTION和SYSFS。而赋值部分有一项,是IMPORT。这个规则的意思是:当系统中出现的新硬件属于net子系统范畴,系统对该硬件采取的动作是加入这个硬件,且这个硬件在sysfs文件系统中的“address”信息等于“00:0d:87:f6:59:f3”时,对这个硬件在udev层次施行的动作是调用外部程序/sbin/rename_netiface,并给该程序传递两个参数,一个是“%k”,代表内核对该新设备定义的名称,另一个是“eth0”。

        通过一个简单的例子可以看出udev和devfs在命名方面的差异。如果系统中有两个USB打印机,一个被称为/dev/usb/lp0,则另一个便是/dev/usb/lp1。但是哪个文件对应哪个打印机是无法确定的,lp0、lp1和实际的设备没有一一对应的关系,映射关系会因为设备发现的顺序、打印机本身关闭等原因而不确定。因此,理想的方式是两个打印机应该采用基于它们的序列号或者其他标识信息的办法来进行确定的映射,devfs无法做到这一点,udev却可以做到。使用如下规则:

BUS="usb", SYSFS{serial}="HXOLL0012202323480", NAME="lp_epson", SYMLINK="printers/epson_stylus"

        该规则中的匹配项目有BUS和SYSFS,赋值项目是NAME和SYMLINK,它意味着当一台USB打印机的序列号为“HXOLL0012202323480”时,创建/dev/lp_opson文件,并同时创建一个符号链接/dev/printers/epson_styles。序列号为“HXOLL0012202323480”的USB打印机不管何时被插入,对应的设备名都是/dev/lp_epson,而devfs显然无法实现设备的这种固定命名。

        udev规则的写法非常灵活,在匹配部分,可以通过“*”、“?”、[a-c]、[1-9]等shell通配符来灵活匹配多个项目。“*”类似于shell中的“*”通配符,替代任意长度的任意字符串,“?”替代一个字符,[x-y]是访问定义。此外,“%k”就是KERNEL,“%n”则是设备的KERNEL序号(如存储设备的分区号)。

        可以借助udev中的udevinfo工具查找规则文件可以利用的信息。

 

五、创建和配置udev

        udev包括udev、udevcontrol、udevd、udevsend、udevmonitor、udevsettle、udevstart、udevinfo、udevtest和udevtrigger等处于用户空间的程序。在嵌入式系统中,只需要udevd和udevstart就能使udev工作。我们可以按照下面的步骤来生成和配置udev。

        (1)下载udev程序,如笔者下载的是udev-114.tar.gz。

        (2)运行“tar zxvf udev-114.tar.gz”命令来解压缩udev程序包。

        (3)运行make编译udev,当前目录下会生成test-udev、udevcontrol、udevd、udevinfo、udevmonitor、udevsettle、udevstart、udevtest和udevtrigger共9个工具程序。

        (4)把第(3)步生成的工具程序复制到/sbin目录,同时把解压缩udev-114.tar.gz后获得的etc目录下的udev目录复制到系统的/etc下,etc/udev下包含udev.conf配置文件、rules.d目录(该目录中的文件给出了设备规则)等。

        (5)编写完成启动、停止、重新启动等工作的udev脚本。

 

 

5.5 总结

        Linux系统用户空间的文件编程有两种方法,即通过Linux API和通过C库函数访问文件。用户空间看不到设备驱动,能看到的只有设备对应的文件,因此文件编程即是用户空间的设备编程。

        Linux系统按照功能对文件系统的目录结构进行了良好的规划。/dev是设备文件的存放目录,devfs和udev分别是Linux2.4设备和Linux2.6设备生成设备文件节点的方法,前者运行于内核空间,后者运行于用户空间。

        Linux2.6设备通过一系列数据结构定义了设备模型,设备模型与sysfs文件系统中的目录和文件存在一种对应关系,udev可以利用sysfs中记录的信息定义规则并提取主次设备号动态创建/dev设备文件节点。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐