- Linux设备驱动开发详解(第2版)
- 华清远见嵌入式培训中心 宋宝华编著
- 352字
- 2020-06-26 03:06:15
第5章 Linux文件系统与设备文件系统
本章导读
由于字符设备和块设备都良好地体现了“一切都是文件”的设计思想,掌握Linux文件系统、设备文件系统的知识就显得相当重要了。
首先,驱动工程师编写的驱动最终通过操作系统的文件操作系统调用或C库函数(本质也基于系统调用)被访问,而设备驱动的结构最终也是为了迎合提供给应用程序员的API。
其次,驱动工程师在设备驱动中不可避免地会与设备文件系统打交道,从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.5节讲解了LDD6410的SD卡和NAND分区和文件系统的使用情况。
5.1 Linux文件操作
5.1.1 文件操作系统调用
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_CREATE标志,则使用的函数是int open(const char *pathname,int flags,mode_t mode); 这个时候我们还要指定mode标志,用来表示文件的访问权限。mode可以是如表5.2所列值的组合。
表5.2 文件访问权限
除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以自己用数字来表示,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, 10 705);
上述语句等价于:
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实际上实现了文件创建的功能,因此,下面的函数等同creat()函数:
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_CUR:相对文件读写指针的当前位置
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, software weekly”,关闭该文件。再次打开该文件,读取其中的内容并输出在屏幕上。
解答如代码清单5.1。
代码清单5.1 Linux文件操作用户空间编程(使用系统调用)
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #define LENGTH 100 6 main() 7 { 8 int fd, len; 9 char str[LENGTH]; 10 11 fd = open("hello.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); /* 12 创建并打开文件 */ 13 if (fd) { 14 write(fd, "Hello World", strlen("Hello World")); /* 15 写入字符串 */ 16 close(fd); 17 } 18 19 fd = open("hello.txt", O_RDWR); 20 len = read(fd, str, LENGTH); /* 读取文件内容 */ 21 str[len] = '\0'; 22 printf("%s\n", str); 23 close(fd); 24 }
编译并运行,执行结果为输出“Hello World”。
5.1.2 C库文件操作
C库函数的文件操作实际上是独立于具体的操作系统平台的,不管是在DOS、Windows、Linux还是在VxWorks中都是这些函数:
1.创建和打开
FILE *fopen(const char *path, const char *mode);
fopen()实现打开指定文件filename,其中的mode为打开模式,C库函数中支持的打开模式如表5.3所示。
表5.3 C库函数文件打开标志
其中 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);
例程:将第5.1.1节中的例程用C库函数来实现,如代码清单5-2所示。
代码清单5.2 Linux文件操作用户空间编程(使用C库函数)
1 #include <stdio.h> 2 #define LENGTH 100 3 main() 4 { 5 FILE *fd; 6 char str[LENGTH]; 7 8 fd = fopen("hello.txt", "w+"); /* 创建并打开文件 */ 9 if (fd) { 10 fputs("Hello World", fd); /* 写入字符串 */ 11 fclose(fd); 12 } 13 14 fd = fopen("hello.txt", "r"); 15 fgets(str, LENGTH, fd); /* 读取文件内容 */ 16 printf("%s\n", str); 17 fclose(fd); 18 }
5.2 Linux文件系统
5.2.1 Linux文件系统目录结构
进入Linux根目录(即“/”,Linux文件系统的入口,也是处于最高一级的目录),运行“ls -l”命令,看到Linux包含以下目录。
1./bin
包含基本命令,如 ls、cp、mkdir等,这个目录中的文件都是可执行的。
2./sbin
包含系统命令,如modprobe、hwclock、ifconfig等,大多是涉及系统管理的命令,这个目录中的文件都是可执行的。
3./dev
设备文件存储目录,应用程序通过对这些文件的读写和控制就可以访问实际的设备。
4./etc
系统配置文件的所在地,一些服务器的配置文件也在这里,如用户账号及密码配置文件。busybox的启动脚本也存放在该目录。
5./lib
系统库文件存放目录,如LDD6410包含libc-2.6.1.so、libpthread-2.6.1.so、libthread_db-1.0.so等。
6./mnt
/mnt这个目录一般是用于存放挂载储存设备的挂载目录的,比如有cdrom 等目录。可以参看/etc/fstab 的定义。有时我们可以把让系统开机自动挂载文件系统,把挂载点放在这里也是可以的。
7./opt
opt 是“可选”的意思,有些软件包会被安装在这里,例如,在LDD6410的文件系统中, Qt/Embedded就存放在该目录。
8./proc
操作系统运行时,进程及内核信息(比如CPU、硬盘分区、内存信息等)存放在这里。/proc目录为伪文件系统proc的挂载目录,proc并不是真正的文件系统,它存在于内存之中。
9./tmp
有时用户运行程序的时候,会产生临时文件,/tmp就用来存放临时文件的。
10./usr
这个是系统存放程序的目录,比如用户命令、用户库等。LDD6410的usr包括bin、sbin、lib三个子目录。usr/bin中包含diff、which、who、rx、cmp等,usr/sbin中包含chroot、flash_eraseall、inetd等,usr/lib中包含libjpeg.so.62.0.0等。
11./var
var 表示的是变化的意思,这个目录的内容经常变动,如/var的/var/log 目录被用来存放系统日志。
12./sys
Linux2.6内核所支持的sysfs文件系统被映射在此目录。Linux设备驱动模型中的总线、驱动和设备都可以在 sysfs 文件系统中找到对应的节点。当内核检测到在系统中出现了新设备后,内核会在sysfs文件系统中为该新设备生成一项新的记录。
5.2.2 Linux文件系统与设备驱动
图5.1所示为Linux中虚拟文件系统、磁盘文件(存放于Ramdisk、Flash、ROM、SD卡、U盘等文件系统中的文件也属于此列)及一般的设备文件与设备驱动程序之间的关系。
图5.1 文件系统与设备驱动
应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。
由于字符设备的上层没有磁盘文件系统,所以字符设备的file_operations 成员函数就直接由设备驱动提供了,在稍后的第6章,将会看到file_operations正是字符设备驱动的核心。
而对于块存储设备而言,ext2、fat、jffs2等文件系统中会实现针对VFS的file_operations成员函数,设备驱动层将看不到 file_operations 的存在。磁盘文件系统和设备驱动会将对磁盘上文件的访问最终转换成对磁盘上柱面和扇区的访问。
在设备驱动程序的设计中,一般而言,会关心file和inode这两个结构体。
1.file结构体
file结构体代表一个打开的文件(设备对应于设备文件),系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file的指针通常被命名为file或filp(即file pointer)。代码清单5.3给出了文件结构体的定义。
代码清单5.3 文件结构体
1 struct file 2 { 3 union { 4 struct list_head fu_list; 5 struct rcu_head fu_rcuhead; 6 } f_u; 7 struct dentry *f_dentry; /*与文件关联的目录入口(dentry)结构*/ 8 struct vfsmount *f_vfsmnt; 9 struct file_operations *f_op; /* 和文件关联的操作*/ 10 atomic_t f_count; 11 unsigned int f_flags;/*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/ 12 mode_t f_mode; /*文件读/写模式,FMODE_READ和FMODE_WRITE*/ 13 loff_t f_pos; /* 当前读写位置*/ 14 struct fown_struct f_owner; 15 unsigned int f_uid, f_gid; 16 struct file_ra_state f_ra; 17 18 unsigned long f_version; 19 void *f_security; 20 21 /* tty驱动需要,其他的也许需要 */ 22 void *private_data; /*文件私有数据*/ 23 ... 24 struct address_space *f_mapping; 25 };
文件读/写模式 mode、标志 f_flags 都是设备驱动关心的内容,而私有数据指针 private_data在设备驱动中被广泛应用,大多被指向设备驱动自定义用于描述设备的结构体。
驱动程序中经常会使用如下类似的代码来检测用户打开文件的读写方式:
if (file->f_mode & FMODE_WRITE) {/* 用户要求可写 */ } if (file->f_mode & FMODE_READ) {/* 用户要求可读 */ }
下面的代码可用于判断以阻塞还是非阻塞方式打开设备文件:
if (file->f_flags & O_NONBLOCK) /* 非阻塞 */ pr_debug("open: non-blocking\n"); else /* 阻塞 */ pr_debug("open: blocking\n");
2.inode结构体
VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁,inode结构体的定义如代码清单5.4所示。
代码清单5.4 inode结构体
1 struct inode { 2 ... 3 umode_t i_mode; /* inode的权限 */ 4 uid_t i_uid; /* inode拥有者的id */ 5 gid_t i_gid; /* inode所属的群组id */ 6 dev_t i_rdev; /* 若是设备文件,此字段将记录设备的设备号 */ 7 loff_t i_size; /* inode所代表的文件大小 */ 8 9 struct timespec i_atime; /* inode最近一次的存取时间 */ 10 struct timespec i_mtime; /* inode最近一次的修改时间 */ 11 struct timespec i_ctime; /* inode的产生时间 */ 12 13 unsigned long i_blksize; /* inode在做I/O时的区块大小 */ 14 unsigned long i_blocks; /* inode所使用的block数,一个block为512 byte*/ 15 16 struct block_device *i_bdev; 17 /*若是块设备,为其对应的block_device结构体指针*/ 18 struct cdev *i_cdev; /*若是字符设备,为其对应的cdev结构体指针*/ 19 ... 20 };
对于表示设备文件的inode结构,i_rdev字段包含设备编号。Linux2.6设备编号分为主设备编号和次设备编号,前者为dev_t的高12位,后者为dev_t的低20位。下列操作用于从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);
查看/proc/devices文件可以获知系统中注册的设备,第1列为主设备号,第2列为设备名,如:
Character devices: 1 mem 2 pty 3 ttyp 4 /dev/vc/0 4 tty 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 21 sg 29 fb 128 ptm 136 pts 171 ieee1394 180 usb 189 usb_device Block devices: 1 ramdisk 2 fd 8 sd 9 md 22 ide1 ...
查看/dev目录可以获知系统中包含的设备文件,日期的前两列给出了对应设备的主设备号和次设备号:
crw-rw---- 1 root uucp 4, 64 Jan 30 2003 /dev/ttyS0 brw-rw---- 1 root disk 8, 0 Jan 30 2003 /dev/sda
主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,不同类的设备一般使用不同的主设备号(但是也不排除在同一主设备号下包含有一定差异的设备)。因为同一驱动可支持多个同类设备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。
内核Documents目录下的devices.txt文件描述了Linux设备号的分配情况,它由LANANA(The Linux Assigned Names And Numbers Authority,网址:http://www.lanana.org/)组织维护,Torben Mathiasen(device@lanana.org)是其中的主要维护者。
5.3 devfs设备文件系统
devfs(设备文件系统)是由Linux2.4内核引入的,引入时被许多工程师给予了高度评价,它的出现使得设备驱动程序能自主地管理它自己的设备文件。具体来说,devfs具有如下优点。(1)可以通过程序在设备初始化时在/dev 目录下创建设备文件,卸载设备时将它删除。
(2)设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。
(3)不再需要为设备驱动程序分配主设备号以及处理次设备号,在程序中可以直接给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的使用范例
1 static devfs_handle_t devfs_handle; 2 static int __init xxx_init(void) 3 { 4 int ret; 5 int i; 6 /*在内核中注册设备*/ 7 ret = register_chrdev(XXX_MAJOR, DEVICE_NAME, &xxx_fops); 8 if (ret < 0) { 9 printk(DEVICE_NAME " can't register major number\n"); 10 return ret; 11 } 12 /*创建设备文件*/ 13 devfs_handle =devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, 14 XXX_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &xxx_fops, NULL); 15 ... 16 printk(DEVICE_NAME " initialized\n"); 17 return 0; 18 } 19 20 static void __exit xxx_exit(void) 21 { 22 devfs_unregister(devfs_handle); /*撤销设备文件*/ 23 unregister_chrdev(XXX_MAJOR, DEVICE_NAME); /*注销设备*/ 24 } 25 26 module_init(xxx_init); 27 module_exit(xxx_exit);
代码中第7行和第23行分别用于注册和注销字符设备,使用的register_chrdev()和unregister_chrdev()在Linux2.6内核中虽然仍然被支持,但这是过时的做法。第13和22行分别用于创建和删除devfs文件节点。
5.4 udev设备文件系统
5.4.1 udev与devfs的区别
尽管devfs有这样和那样的优点,但是,在Linux2.6内核中,devfs被认为是过时的方法,并最终被抛弃,udev取代了它。Linux VFS内核维护者Al Viro指出了几点udev取代devfs的原因:(1)devfs所做的工作被确信可以在用户态来完成。
(2)devfs被加入内核之时,大家寄望它的质量可以迎头赶上。
(3)devfs被发现了一些可修复和无法修复的bug。
(4)对于可修复的bug,几个月前就已经被修复了,其维护者认为一切良好。
(5)对于后者,同样是相当长一段时间以来没有改观了。
(6)devfs 的维护者和作者对它感到失望并且已经停止了对代码的维护工作。
Linux内核的两位贡献者,Richard Gooch(devfs的作者)和Greg Kroah-Hartman(sysfs的主要作者)就devfs/udev进行了激烈的争论:
Greg:Richard had stated that udev was a proper replacement for DevFS.
Richard:Well, that's news to me!
Greg:DevFS should be taken out because policy should exist in userspace and not in the kernel.
Richard:SysFS, developed in large part by Greg, also implemented policy in the kernel.
Greg:DevFS was broken and unfixable
Richard:No proof. Never say never...
这段有趣的争论可意译如下:
Greg:Richard已经指出,udev是DevFS恰当的替代品。
Richard:哦,是哪个Richard说的?我怎么不知道。
Greg:DevFS应该下课,因为策略应该位于用户空间而不是内核空间。
Richard:哦,我听说,相当大部分由Greg完成的sysfs也在内核中实现了策略。
Greg:devfs很蹩脚,也不稳定。
Richard:呵呵,没证据,别那么武断……
在Richard Gooch和Greg Kroah-Hartman的争论中,Greg Kroah-Hartman使用的理论依据就在于policy(策略)不能位于内核空间。Linux设计中强调的一个基本观点是机制和策略的分离。机制是做某样事情的固定的步奏、方法,而策略就是每一个步奏所采取的不同方式。机制是相对固定的,而每个步奏采用的策略是不固定的。机制是稳定的,而策略则是灵活的,因此,在 Linux内核中,不应该实现策略。Richard Gooch认为,属于策略的东西应该被移到用户空间。这就是为什么devfs位于内核空间,而udev确要移到用户空间的原因。
下面举一个通俗的例子来理解udev设计的出发点。以谈恋爱为例,Greg Kroah-Hartman认为,可以让内核提供谈恋爱的机制,但是不能在内核空间限制跟谁谈恋爱,不能把谈恋爱的策略放在内核空间。因为恋爱是自由的,用户应该可以在用户空间中实现“萝卜白菜,各有所爱”的理想,可以根据对方的外貌、籍贯、性格等自由选择。对应devfs而言,第1个相亲的女孩被命名为/dev/girl0,第2个相亲的女孩被命名为/dev/girl1,依此类推。而在用户空间实现的udev则可以使得用户实现这样的自由:不管你中意的女孩第几个来,只要它与你定义的规则符合,都命名为/dev/mygirl!
udev 完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件(hotplug event)来工作。在热插拔时,设备的详细信息会由内核输出到位于/sys的sysfs文件系统。udev的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用sysfs中的信息来进行创建设备文件节点等工作。热插拔时输出到sysfs中的设备的详细信息就是相亲对象的资料(外貌、年龄、性格、籍贯等),设备命名策略等就是择偶标准。devfs 是个蹩脚的婚姻介绍所,它直接指定了谁和谁谈恋爱,而udev则聪明地多,它只是把资料交给客户,让客户根据这些资料去选择和谁谈恋爱。
由于udev根据系统中硬件设备的状态动态更新设备文件,进行设备文件的创建和删除等,因此,在使用udev后,/dev目录下就会只包含系统中真正存在的设备了。
devfs与udev的另一个显著区别在于:采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动,而udev则不这么做。这是因为udev的设计者认为Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候。udev 的设计者认为 devfs 所提供的打开/dev节点时自动加载驱动的功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生热插拔事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
5.4.2 sysfs文件系统与Linux设备模型
Linux2.6的内核引入了sysfs文件系统,sysfs被看成是与proc、devfs和devpty同类别的文件系统,该文件系统是一个虚拟的文件系统,它可以产生一个包括所有系统硬件的层级视图,与提供进程和状态信息的proc文件系统十分类似。
sysfs把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性。sysfs的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括block、device、bus、drivers、class、power和firmware。
block 目录包含所有的块设备;devices 目录包含系统所有的设备,并根据设备挂接的总线类型组织成层次结构;bus 目录包含系统中所有的总线类型;drivers 目录包括内核中所有已注册的设备驱动程序;class目录包含系统中的设备类型(如网卡设备、声卡设备、输入设备等)。在/sys目录运行tree会得到一个相当长的树型目录,下面摘取一部分:
|-- block | |-- fd0 | |-- md0 | |-- ram0 | |-- ram1 | |-- ... |-- bus | |-- eisa | | |-- devices | | '-- drivers | |-- ide | |-- ieee1394 | |-- pci | | |-- devices | | | |-- 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0 | | | |-- 0000:00:01.0 -> ../../../devices/pci0000:00/0000:00:01.0 | | | |-- 0000:00:07.0 -> ../../../devices/pci0000:00/0000:00:07.0 | | '-- drivers | | |-- PCI_IDE | | | |-- bind | | | |-- new_id | | | '-- unbind | | '-- pcnet32 | | |-- 0000:00:11.0 -> ../../../../devices/pci0000:00/0000:00:11.0 | | |-- bind | | |-- new_id | | '-- unbind | |-- platform | |-- pnp | '-- usb | |-- devices | '-- drivers | |-- hub | |-- usb | |-- usb-storage | '-- usbfs |-- class | |-- graphics | |-- hwmon | |-- ieee1394 | |-- ieee1394_host | |-- ieee1394_node | |-- ieee1394_protocol | |-- input | |-- mem | |-- misc | |-- net | |-- pci_bus | | |-- 0000:00 | | | |-- bridge -> ../../../devices/pci0000:00 | | | |-- cpuaffinity | | | '-- uevent | | '-- 0000:01 | | |-- bridge -> ../../../devices/pci0000:00/0000:00:01.0 | | |-- cpuaffinity | | '-- uevent | |-- scsi_device | |-- scsi_generic | |-- scsi_host | |-- tty | |-- usb | |-- usb_device | |-- usb_host | '-- vc |-- devices | |-- pci0000:00 | | |-- 0000:00:00.0 | | |-- 0000:00:07.0 | | |-- 0000:00:07.1 | | |-- 0000:00:07.2 | | |-- 0000:00:07.3 | |-- platform | | |-- floppy.0 | | |-- host0 | | |-- i8042 | |-- pnp0 | '-- system |-- firmware |-- kernel | '-- hotplug_seqnum |-- module | |-- apm | |-- autofs | |-- cdrom | |-- eisa_bus | |-- i8042 | |-- ide_cd | |-- ide_scsi | |-- ieee1394 | |-- md_mod | |-- ohci1394 | |-- parport | |-- parport_pc | |-- usb_storage | |-- usbcore | | |-- parameters | | |-- refcnt | | '-- sections | |-- virtual_root | | '-- parameters | | '-- force_probe | '-- vmhgfs | |-- refcnt | '-- sections | '-- __versions '-- power '-- state
在/sys/bus的pci等子目录下,又会再分出drivers和devices目录,而devices目录中的文件是对/sys/devices目录中文件的符号链接。同样地,/sys/class目录下也包含许多对/sys/devices下文件的链接。如图5.2所示,这与设备、驱动、总线和类的现实状况是直接对应的,也正符合Linux2.6的设备模型。
图5.2 Linux设备模型
随着技术的不断进步,系统的拓扑结构越来越复杂,对智能电源管理、热插拔以及即插即用的支持要求也越来越高,Linux2.4内核已经难以满足这些需求。为适应这种形势的需要,Linux2.6内核开发了上述全新的设备、总线、类和驱动环环相扣的设备模型。图5.3形象地表示了 Linux驱动模型中设备、总线和类之间的关系。
大多数情况下,Linux2.6内核中的设备模型代码会作为“幕后黑手”处理好这些关系,内核中的总线级和其他内核子系统会完成与设备模型的交互,这使得驱动工程师几乎不需要关心设备模型。
在Linux内核中,分别使用bus_type、device_driver和device来描述总线、驱动和设备,这3个结构体定义于include/linux/device.h头文件中,其定义如代码清单5.6所示。
图5.3 Linux驱动模型中设备、总线和类的关系
代码清单5.6 bus_type、device_driver和device结构体
1 struct bus_type { 2 const char *name; 3 struct bus_attribute *bus_attrs; 4 struct device_attribute *dev_attrs; 5 struct driver_attribute *drv_attrs; 6 7 int (*match)(struct device *dev, struct device_driver *drv); 8 int (*uevent)(struct device *dev, struct kobj_uevent_env *env); 9 int (*probe)(struct device *dev); 10 int (*remove)(struct device *dev); 11 void (*shutdown)(struct device *dev); 12 13 int (*suspend)(struct device *dev, pm_message_t state); 14 int (*suspend_late)(struct device *dev, pm_message_t state); 15 int (*resume_early)(struct device *dev); 16 int (*resume)(struct device *dev); 17 18 struct pm_ext_ops *pm; 19 20 struct bus_type_private *p; 21 }; 22 23 struct device_driver { 24 const char *name; 25 struct bus_type *bus; 26 27 struct module *owner; 28 const char *mod_name; 29 30 int (*probe) (struct device *dev); 31 int (*remove) (struct device *dev); 32 void (*shutdown) (struct device *dev); 33 int (*suspend) (struct device *dev, pm_message_t state); 34 int (*resume) (struct device *dev); 35 struct attribute_group **groups; 36 37 struct pm_ops *pm; 38 39 struct driver_private *p; 40 }; 41 42 struct device { 43 struct klist klist_children; 44 struct klist_node knode_parent; 45 struct klist_node knode_driver; 46 struct klist_node knode_bus; 47 struct device *parent; 48 49 struct kobject kobj; 50 char bus_id[BUS_ID_SIZE]; /* 在父总线中的位置 */ 51 const char *init_name; /* 设备的初始名 */ 52 struct device_type *type; 53 unsigned uevent_suppress:1; 54 55 struct semaphore sem; 56 57 struct bus_type *bus; /* 设备所在的总线类型 */ 58 struct device_driver *driver; /* 设备用到的驱动 */ 59 void *driver_data; 60 void *platform_data; 61 struct dev_pm_info power; 62 63 #ifdef CONFIG_NUMA 64 int numa_node; 65 #endif 66 u64 *dma_mask; 67 u64 coherent_dma_mask; 68 69 struct device_dma_parameters *dma_parms; 70 71 struct list_head dma_pools; 72 73 struct dma_coherent_mem *dma_mem; 74 75 struct dev_archdata archdata; 76 77 spinlock_t devres_lock; 78 struct list_head devres_head; 79 80 struct klist_node knode_class; 81 struct class *class; 82 dev_t devt; /* dev_t, 创建sysfs "dev" */ 83 struct attribute_group**groups; 84 85 void(*release)(struct device *dev); 86 };
device_driver和device分别表示驱动和设备,而这两者都必须依附于一种总线,因此都包含struct bus_type指针。在Linux内核中,设备和驱动是分开注册的,注册1个设备的时候,并不需要驱动已经存在,而1个驱动被注册的时候,也不需要对应的设备已经被注册。设备和驱动各自涌向内核,而每个设备和驱动涌入的时候,都会去寻找自己的另一半。茫茫人海,何处觅踪?正是bus_type的match()成员函数将两者捆绑在一起。简单地说,设备和驱动就是红尘中漂浮的男女,而bus_type的match()则是牵引红线的月老,它可以识别什么设备与什么驱动可以配对。
注意,总线、驱动和设备都最终会落实为sysfs中的1个目录,因为进一步追踪代码会发现,它们实际上都可以认为是kobject的派生类(device结构体直接包含了kobject kobj成员,而bus_type和device_driver则透过bus_type_private、driver_private间接包含kobject),kobject可看作所有总线、设备和驱动的抽象基类,1个kobject对应sysfs中的1个目录。
总线、设备和驱动中的各个attribute则直接落实为sysfs中的1个文件,attribute会伴随着show()和 store()这两个函数,分别用于读和写该 attribute 对应的sysfs 文件结点,代码清单5.7给出了attribute、bus_attribute、driver_attribute和device_attribute这几个结构体的定义。
代码清单5.7 attribute、bus attribute、driver attribute和device attribute结构体
1 struct attribute { 2 const char *name; 3 struct module *owner; 4 mode_t mode; 5 }; 6 7 struct bus_attribute { 8 struct attribute attr; 9 ssize_t (*show)(struct bus_type *bus, char *buf); 10 ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count); 11 }; 12 13 struct driver_attribute { 14 struct attribute attr; 15 ssize_t (*show)(struct device_driver *driver, char *buf); 16 ssize_t (*store)(struct device_driver *driver, const char *buf, 17 size_t count); 18 }; 19 20 struct device_attribute { 21 struct attribute attr; 22 ssize_t (*show)(struct device *dev, struct device_attribute *attr, 23 char *buf); 24 ssize_t (*store)(struct device *dev, struct device_attribute *attr, 25 const char *buf, size_t count); 26 };
事实上,udev规则中各信息的来源实际上就是bus_type、device_driver、device以及attribute等所对应sysfs节点。
5.4.3 udev的组成
udev的主页位于:http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html,上面包含了关于 udev 的详细介绍,从 http://www.us.kernel.org/pub/linux/utils/kernel/hotplug/上可以下载最新的udev包。udev的设计目标如下。
● 在用户空间中执行。
(1)动态建立/删除设备文件。
(2)允许每个人都不用关心主/次设备号。
(3)提供LSB标准名称。
(4)如果需要,可提供固定的名称。
为了提供这些功能,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.8给出了一个namedev命名规则的例子,第2、4行定义的是符合第1步的规则,第6、8行定义的是符合第2步的规则,第11、14行定义的是符合第3步的规则,第16行定义的是符合第4步的规则。
代码清单5.8 namedev命名规则
1 # USB Epson printer to be called lp_epson 2 LABEL, BUS="usb", serial="HXOLL0012202323480", NAME="lp_epson" 3 # USB HP printer to be called lp_hp, 4 LABEL, BUS="usb", serial="W09090207101241330", NAME="lp_hp" 5 # sound card with PCI bus id 00:0b.0 to be the first sound card 6 NUMBER, BUS="pci", id="00:0b.0", NAME="dsp" 7 # sound card with PCI bus id 00:07.1 to be the second sound card 8 NUMBER, BUS="pci", id="00:07.1", NAME="dsp1" 9 # USB mouse plugged into the third port of the first hub to be 10 # called mouse0 11 TOPOLOGY, BUS="usb", place="1.3", NAME="mouse0" 12 # USB tablet plugged into the second port of the second hub to be 13 # called mouse1 14 TOPOLOGY, BUS="usb", place="2.2", NAME="mouse1" 15 # ttyUSB1 should always be called visor 16 REPLACE, KERNEL="ttyUSB1", NAME="visor"
5.4.4 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_epson文件,并同时创建一个符号链接/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 工具查找规则文件可以利用的信息,如运行“udevinfo -a -p/sys/block/sda”命令将得到:
Udevinfo starts with the device specified by the devpath and then walks up the chain of parent devices. It prints for every device found, all possible attributes in the udev rules key format. A rule to match, can be composed by the attributes of the device and the attributes from one single parent device. looking at device '/block/sda': KERNEL=="sda" SUBSYSTEM=="block" DRIVER=="" ATTR{stat}==" 1 689 3 169 85 746 24 000 2 017 2 095 32 896 47 292 0 23 188 71 292" ATTR{size}=="6 291 456" ATTR{removable}=="0" ATTR{range}=="16" ATTR{dev}=="8:0" looking at parent device '/devices/platform/host0/target0:0:0/0:0:0:0': KERNELS=="0:0:0:0" SUBSYSTEMS=="scsi" DRIVERS=="sd" ATTRS{ioerr_cnt}=="0x5" ATTRS{iodone_cnt}=="0xe86" ATTRS{iorequest_cnt}=="0xe86" ATTRS{iocounterbits}=="32" ATTRS{timeout}=="30" ATTRS{state}=="running" ATTRS{rev}=="1.0 " ATTRS{model}=="VMware Virtual S" ATTRS{vendor}=="VMware, " ATTRS{scsi_level}=="3" ATTRS{type}=="0" ATTRS{queue_type}=="none" ATTRS{queue_depth}=="3" ATTRS{device_blocked}=="0" looking at parent device '/devices/platform/host0/target0:0:0': KERNELS=="target0:0:0" SUBSYSTEMS=="" DRIVERS=="" looking at parent device '/devices/platform/host0': KERNELS=="host0" SUBSYSTEMS=="" DRIVERS=="" looking at parent device '/devices/platform': KERNELS=="platform" SUBSYSTEMS=="" DRIVERS==""
5.4.5 创建和配置mdev
在嵌入式系统中,通常可以用 udev 的轻量级版本 mdev,mdev 集成于 busybox(本书配套VirtualBox 虚拟机/home/lihacker/develop/svn/ldd6410-read-only/utils/busybox-1.15.1 目录)中。在busybox的源代码目录运行make menuconfig,进入“Linux System Utilities”子选项,选中mdev相关项目,如图5-4所示。
图5.4 busybox中的mdev选项
LDD6410根文件系统中/etc/init.d/rcS包含的如下内容即是为了使用mdev的功能:
/bin/mount -t sysfs sysfs /sys /bin/mount -t tmpfs mdev /dev echo /bin/mdev > /proc/sys/kernel/hotplug mdev -s
其中“mdev -s”的含义是扫描/sys中所有的类设备目录,如果在目录中含有名为“dev”的文件,且文件中包含的是设备号,则mdev就利用这些信息为该设备在/dev下创建设备节点文件。
“echo /sbin/mdev > /proc/sys/kernel/hotplug”的含义是当有热插拔事件产生时,内核就会调用位于 /sbin目录的mdev。这时mdev通过环境变量中的ACTION和DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的那个目录。接着会看看这个目录中是否有“dev”的属性文件,如果有就利用这些信息为这个设备在/dev下创建设备节点文件。
若要修改mdev的规则,可通过修改/etc/mdev.conf文件实现。
5.5 LDD6410的SD和NAND文件系统
LDD6410的SD卡分为两个区,其中的第2个分区为ext3文件系统,存放LDD6410的文件数据(5.2.1节给出的各个目录),其制作方法如下。
(1)在安装了Linux的PC机上通过fdisk给一张空的SD卡分为2个区(如果SD卡中本身已经包含,请通过fdisk的“d”命令全部删除),得到如下的分区表:
Command (m for help): p Disk /dev/sdb: 1 030 MB, 1 030 225 920 bytes 32 heads, 62 sectors/track, 1 014 cylinders Units = cylinders of 1984 * 512 = 1 015 808 bytes Disk identifier: 0x6f20736b Device Boot Start End Blocks Id System /dev/sdb1 * 1 20 19 809 83 Linux /dev/sdb2 21 1014 98 6048 83 Linux
注意第1个分区制作的命令为:
Command (m for help): n Command action e extended p primary partition (1-4) p Partition number (1-4): 1 First cylinder (1-1 014, default 1): Using default value 1 Last cylinder, +cylinders or +size{K,M,G} (1-1 014, default 1 014): 20M
第2个分区制作的命令是:
Command (m for help): n Command action e extended p primary partition (1-4) p Partition number (1-4): 2 First cylinder (21-1 014, default 21): Using default value 21 Last cylinder, +cylinders or +size{K,M,G} (21-1 014, default 1 014): Using default value 1 014 Command (m for help):
我们还要通过“a”命令标记第1个分区:
Command (m for help): a Partition number (1-4): 1
最后要通过“w”命令把建好的分区表写入SD卡。
(2)格式化SD卡的分区1和分区2:
mkfs.vfat /dev/sdb1 mkfs.ext3 /dev/sdb2 fsck.ext3 /dev/sdb2
(3)如图5-5所示,通过moviNAND_Fusing_Tool.exe烧写SD卡U-BOOT和zImage。
图5.5 烧写LDD6410的SD卡U - BOOT和zImage
更新SD卡根文件系统的方法很简单,在PC机上mount /dev/sdb2后,直接通过
cp -fa <your-rootfs> <sdb2-mount-point>
即可替换根文件系统了。<your-rootfs>是根文件系统的目录,<sdb2-mount-point>是/dev/sdb2挂载的目录。
特别要注意的是,SD的设备节点不一定是/dev/sdb,应该视用户电脑的硬盘情况而言,可能是/dev/sdc、/dev/sdd 等。
LDD6410的NAND分为3个区,分别存放U-BOOT、zImage和文件系统。该分区表定义在LDD6410的BSP中:
struct mtd_partition s3c_partition_info[] = { { .name = "Bootloader", .offset = 0, .size = (512*SZ_1K), }, { .name = "Kernel", .offset = (512*SZ_1K), .size = (4*SZ_1M) - (512*SZ_1K), }, { .name = "File System", .offset = MTDPART_OFS_APPEND, .size = MTDPART_SIZ_FULL, } };
更新NAND中U-BOOT的方法如下:
(1)通过 tftp或nfs 等方式获取新的U-BOOT,如:
# tftp -r u-boot-movi.bin -g 192.168.1.111
(2)运行:
# flashcp u-boot-movi.bin /dev/mtd0
更新NAND中zImage的方法如下:
(1)通过 tftp或nfs 等方式获取新的zImage,如:
# tftp -r zImage-fix -g 192.168.1.111
(2)运行:
# flashcp zImage-fix /dev/mtd1
更新NAND中文件系统的方法如下:
在PC上将做好的新的根文件系统拷贝到SD卡或NFS的某目录,下面我们以<new_rootfs_dir>指代该目录。
以SD卡或NFS为根文件系统启动系统,运行如下命令擦除/dev/mtd2分区:
# flash_eraseall /dev/mtd2
然后将NAND的该分区mount到/mnt:
# mount /dev/mtdblock2 -t yaffs2 /mnt/
将新的文件系统拷贝到/mnt:
# cp -fa <new_rootfs_dir> /mnt
若上述命令运行过程中设备结点不存在,可先执行:
# mdev -s
启动到LDD6410,在根目录下运行ls,会发现LDD6410根文件系统包含如下子目录:
#1s android init.rc sbin bin lib sqlite_stmt_journals cache linuxrc sys data lost+found system demo mnt tmp dev opt usr etc proc var init.goldfish.rc qtopia
其中的/data /cache /system /sqlite_stmt_journals 为 Android 所需要的目录,/opt 下存放Qt/Embedded 的可执行文件。运行“/android&”可启动 Android,运行“/qtopia &”可启动Qt/Embedded。
/demo目录下存放在一些用于demo的图片、MP3等,如运行如下命令可显示jpeg图片1.jpg、2.jpg、3.jpg和4.jpg。
# cd /demo/ # jpegview 1.jpg 2.jpg 3.jpg 4.jpg 480 272 480 544 0 272 16 0 480 272 framebase = 0x4016c000 err=0 ImageWidth=362 ImageHeight=272 read 1.jpg OK ImageWidth=362 ImageHeight=272 read 2.jpg OK ImageWidth=362 ImageHeight=272 read 3.jpg OK ImageWidth=362 ImageHeight=272 read 4.jpg OK
5.6 总结
Linux用户空间的文件编程有两种方法,即通过Linux API和通过C库函数访问文件。用户空间看不到设备驱动,能看到的只有设备对应的文件,因此文件编程即是用户空间的设备编程。
Linux按照功能对文件系统的目录结构进行了良好的规划。/dev是设备文件的存放目录,devfs和udev分别是Linux2.4和Linux2.6生成设备文件节点的方法,前者运行于内核空间,后者运行于用户空间。
Linux2.6通过一系列数据结构定义了设备模型,设备模型与sysfs文件系统中的目录和文件存在一种对应关系,udev 可以利用 sysfs 中记录的信息定义规则并提取主次设备号动态创建/dev设备文件节点。