2017-05-13 15:16:49 GeekWill 阅读数 559

  Linux支持许多文件系统,从日志文件系统到集群文件系统和加密文件系统。对于使用标准的和比较奇特的文件系统以及开发文件系统来说,Linux是一个很好的平台。
  Linux文件系统结构是一个对复杂系统进行抽象化的有趣例子。通过使用一组通用的API函数,Linux可以在许多存储设备上支持许多的文件系统。例如,read 函数调用可以从指定的文件描述符读取一定数量的字节。read 函数不了解文件系统的类型,比如 ext3 或 NFS。它也不了解文件系统所在的存储媒体,比如 AT Attachment Packet Interface(ATAPI)磁盘、Serial-Attached SCSI(SAS)磁盘或 Serial Advanced Technology Attachment(SATA)磁盘。但是,当通过调用 read 函数读取一个文件时,数据会正常返回。
  Linux系统存在很多的文件系统,例如常见的ext2、ext3、sysfs、rootfs、proc…Linux 中允许众多不同的文件系统共存,如 ext2, ext3, vfat 等。通过使用同一套文件 I/O 系统 调用即可对 Linux 中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,对文件的 操作可以跨文件系统而执行。如图 1 所示,我们可以使用 cp 命令从 vfat 文件系统格式的硬盘拷贝数据到 ext3 文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统。

1、Linux虚拟文件系统(VFS)

VFS概念:

  VFS:Virtual File System虚拟文件系统,或Virtual File Switch虚拟文件转换/开关。VFS是Linux和UNIX文件系统中采用的一种技术机制,旨在一个操作系统中支持多个不同类型的文件系统。VFS是操作系统内核中这样一组数据结构与子程序的集合,它位于操作系统系统调用界面与具体类型文件系统之间,负责记录操作系统中可以支持和已经安装有哪些文件系统类型,将相关系统调用转换为对具体类型文件系统的调用,负责不同类型文件系统间的协同工作(例如跨FS复制),实现对不同类型文件系统的动态装卸和可扩充性等。

VFS功能:

  • 向用户、应用程序、和操作系统其他部件提供了一个通用的、统一的、标准的、抽象的、虚拟的系统调用接口界面(所以称Virtual)。
  • 对以上应用程序等掩盖不同类型文件系统的差异和细节。为以上应用程序等提供了对具体文件系统类型的程序独立性和透明性。
  • 例如,当用户程序AP1在两次运行中分别读EXT2、NTFS文件,都使用同样的read(…)系统调用函数,程序AP1不必改变

Linux中VFS示意图:

这里写图片描述
这里写图片描述

Linux的VFS目前支持的文件系统

  目前至少50多种,可从fs.h中或fs/*.c或/proc/filesystems看到:
  a、本地文件系统:EXT2,EXT3,EXT4,FAT,NTFS,minix,UFS,HFS,ISOFS,HPFS,AFFS(FFS),SYSV(S5FS),EFS,UDF等。
  b、网络文件系统:NFS,coda,SAMBA,等
  c、虚拟文件系统:PROC,等

2、VFS数据结构:

  

A、VFS基本数据结构:


  Linux虚拟文件系统四大对象:超级块、索引节点、目录项、文件对象。
  从本质上讲,文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。为了描述 这个结构,Linux引入了一些基本概念:
  文件:一组在逻辑上具有完整意义的信息项的系列。在Linux中,除了普通文件,其他诸如目录、设备、套接字等 也以文件被对待。总之,“一切皆文件”。
  目录:目录好比一个文件夹,用来容纳相关文件。因为目录可以包含子目录,所以目录是可以层层嵌套,形成 文件路径。在Linux中,目录也是以一种特殊文件被对待的,所以用于文件的操作同样也可以用在目录上。
  目录项:在一个文件路径中,路径中的每一部分都被称为目录项;如路径/home/source/helloworld.c中,目录 /, home, source和文件 helloworld.c都是一个目录项。
  索引节点:用于存储文件的元数据的一个数据结构。文件的元数据,也就是文件的相关信息,和文件本身是两个不同 的概念。它包含的是诸如文件的大小、拥有者、创建时间、磁盘位置等和文件相关的信息。
  超级块:用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节 点数等,存放于磁盘的特定扇区中。
  超级块:一个超级块对应一个文件系统(已经安装的文件系统类型如ext2,此处是实际的文件系统哦,不是VFS)。之前我们已经说了文件系统用于管理这些文件的数据格式和操作之类的,系统文件有系统文件自己的文件系统,同时对于不同的磁盘分区也有可以是不同的文件系统。
  那么一个超级块对于一个独立的文件系统。保存文件系统的类型、大小、状态等等
  (“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block),即同一个文件类型下可以有多个文件系统实体(关于这句话的理解仔细理解下图可得)
  既然我们知道对于不同的文件系统有不同的super_block,那么对于不同的super_block的操作肯定也是不同的,所以我们在下面的super_block结构中可以看到上面说的抽象的struct结构(例如下面的:struct super_operations)。
  对于VFS我们知道,与用户打交道的是VFS,然后VFS与底层真正的文件系统交流。那我们知道在一个VFS下面,允许存在很多的“文件系统类型”,例如ext2,ext3,ext4,sysfs,proc等等。这些类型是可以共存的,同时,对于每一个类型来说,可以存在多个文件系统实体,例如:在一个目录下有多个子目录,子目录之间的文件系统类型可以不一样,也可以有部分是一样的类型,借用windows中的例子说就是,D盘和E盘可以都是NTFS类型文件系统,也可以是不一样的文件类型系统。在Linux中,系统有一个全局变量叫做file_systems,这个变量用来管理所有的“文件系统类型”链表。也就是所有的文件系统类型都必须注册到(链接到)这个链表中,才可以被使用。如果是自己的文件系统,只要符合VFS的标准,也是可以注册进去的。最终形成一个单链表形式结构。
  这里写图片描述

Linux下VFS的实现细节

  如上的数据结构并不是孤立存在的。正是通过它们的有机联系,VFS才能正常工作。如下的几张图是对它们之间的联系的描述。
  如图5所示,被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统 中。每安装一个文件系统,就对应有一个超级块和安装点。超级块通过它的一个域s_type指向其对应的具体的文件系统类型。具体的 文件系统通过file_system_type中的一个域fs_supers链接具有同一种文件类型的超级块。同一种文件系统类型的超级块通过域s_instances链接。
  这里写图片描述
  从图6可知:进程通过task_struct中的一个域files_struct files来了解它当前所打开的文件对象;而我们通常所说的文件 描述符其实是进程打开的文件对象数组的索引值。文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找 到它对应的索引结点,这样就建立了文件对象与实际的物理文件的关联。最后,还有一点很重要的是, 文件对象所对应的文件操作函数列表是通过索引结点的域i_fop得到的。图6对第三部分源码的理解起到很大的作用。
这里写图片描述
这里写图片描述

//进程表的相关内容
Struct task_struct{
…………..........
/* filesystem information */文件系统信息指针
struct fs_struct *fs;
/* open file information */进程打开的文件对象指针
struct files_struct *files;
……………....
}
//struct fs_struct
struct fs_struct 
{
    atomic_t count;
    rwlock_t lock;
    int umask;
    struct dentry * root, * pwd, * altroot;
    struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
//进程打开文件表Struct files_struct
Struct files_struct
{
        atomic_t count;
        rwlock_t file_lock;
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file **fd;/*current fd array*/
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OREN_DEFAULT];
}
//进程打开文件表struct file
struct file 
{
    /*
     * fu_list becomes invalid after file_free is called and queued via
     * fu_rcuhead for RCU freeing
     */
    union {
        struct list_head    fu_list;
        struct rcu_head     fu_rcuhead;
} f_u;
    struct path     f_path;
#define f_dentry    f_path.dentry
#define f_vfsmnt    f_path.mnt
    const struct file_operations    *f_op;
    atomic_t        f_count;
    unsigned int        f_flags;
    mode_t          f_mode;
    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;
#ifdef CONFIG_SECURITY
    void            *f_security;
#endif
    /* needed for tty driver, and maybe others */
    void            *private_data;
#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    spinlock_t      f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

  这里写图片描述

Linux下的多进程与多文件之间的关系

  对于一个进程来说,可以打开多个文件,也可以多个进程打开一个文件,对于文件来说,不变的永远是自己的inode节点,变化的仅仅是和进程直接关系的file结构。可以看一下下面的大图:
  这里写图片描述
  VFS的OO(面向对象的思想):
- 系统打开文件表:目录项对象(dentry 对象)
- 内存活动I结点表:inode对象
- 进程打开文件表:文件对象(file对象)
- 存超级块表:超级块对象

A、关于目录项

  目录:文件是通过目录组织起来的,目录相当于文件夹,用来容纳对应的文件。/home/csj/1.c。根目录是’/’,后面的home,csj也是目录,最后的1.c表示一个程序文件,目录也可以包含子目录,目录从而形成层层嵌套,形成文件路径。
  目录项:路径中的每一个部分(/,home,csj,1.c)都被称为目录条目(也即目录项)。从而得知,目录项中可以包含普通文件如1.c,故目录不等同于目录项。目录项对象:引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是 普通的文件,都是一个目录项对象。如,在路径/home/source/test.c中,目录 /, home, source和文件 test.c都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS在遍 历路径名的过程中现场将它们逐个地解析成目录项对象。
  目录也即是文件(即目录文件),“一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。因而每个文件对应一个索引节点,所以路径/home/csj/1.c中包含的目录项各对应一个索引节点(/,home,csj这三个是目录文件,1.c相当于普通文件)。

B、文件的查找

  VFS是如何从文件路径名找到相应的索引节点的?
- 分析路径名,将它拆分成一个文件名序列
- 除了最后一个文件名以外,其他所有文件名必定都是目录名。
搜索的起点:
- 绝对路径:current->fs->root
- 相对路径:current->fs->pwd
首先找到起点目录的索引节点。然后在这个索引节点的目录文件中,找到第一个目录名(你要找的文件的第一个目录项)所对应的索引节点在第一个目录名的索引节点的目录文件中找到第二个目录名(文件名)所对应的索引节点。
…反复,直到文件名序列的最后一项

struct inode {  
         struct list_head        i_hash;  
         struct list_head        i_list;  
        struct list_head        i_dentry;  
         //指向目录项链表指针,注意一个inodes可以对应多个dentry,因为一个实际的文件可能被链接到其他的文件,那么就会有另一 //个dentry,这个链表就是将所有的与本inode有关的dentry都连在一起。 
        struct list_head        i_dirty_buffers;  
        struct list_head        i_dirty_data_buffers;  

         unsigned long           i_ino;  
         atomic_t                i_count;  
         kdev_t                  i_dev;  
         umode_t                 i_mode;  
         unsigned int            i_nlink;  
         uid_t                   i_uid;  
         gid_t                   i_gid;  
         kdev_t                  i_rdev;  
         loff_t                  i_size;  
         time_t                  i_atime;  
         time_t                  i_mtime;  
         time_t                  i_ctime;  
         unsigned int            i_blkbits;  
         unsigned long           i_blksize;  
         unsigned long           i_blocks;  
         unsigned long           i_version;  
         unsigned short          i_bytes;  
         struct semaphore        i_sem;  
         struct rw_semaphore     i_alloc_sem;  
         struct semaphore        i_zombie;  
         struct inode_operations *i_op;  
         struct file_operations  *i_fop; /* former ->i_op->default_file_ops */  
        struct super_block      *i_sb;  
         wait_queue_head_t       i_wait;  
         struct file_lock        *i_flock;  
         struct address_space    *i_mapping;  
         struct address_space    i_data;  
         struct dquot            *i_dquot[MAXQUOTAS];  
         /* These three should probably be a union */  
         struct list_head        i_devices;  
         struct pipe_inode_info  *i_pipe;  
         struct block_device     *i_bdev;  
         struct char_device      *i_cdev;  

         unsigned long           i_dnotify_mask; /* Directory notify events */  
         struct dnotify_struct   *i_dnotify; /* for directory notifications */  

         unsigned long           i_state;  

         unsigned int            i_flags;  
         unsigned char           i_sock;  

         atomic_t                i_writecount;  
         unsigned int            i_attr_flags;  
         __u32                   i_generation;  
         union {  
                 struct minix_inode_info         minix_i;  
                 struct ext2_inode_info          ext2_i;  
                 struct ext3_inode_info          ext3_i;  
                 struct hpfs_inode_info          hpfs_i;  
                 struct ntfs_inode_info          ntfs_i;  
                 struct msdos_inode_info         msdos_i;  
                 struct umsdos_inode_info        umsdos_i;  
                 struct iso_inode_info           isofs_i;  
                 struct nfs_inode_info           nfs_i;  
                 struct sysv_inode_info          sysv_i;  
                 struct affs_inode_info          affs_i;  
                 struct ufs_inode_info           ufs_i;  
                 struct efs_inode_info           efs_i;  
                 struct romfs_inode_info         romfs_i;  
                 struct shmem_inode_info         shmem_i;  
                 struct coda_inode_info          coda_i;  
                 struct smb_inode_info           smbfs_i;  
                 struct hfs_inode_info           hfs_i;  
                 struct adfs_inode_info          adfs_i;  
                 struct qnx4_inode_info          qnx4_i;  
                 struct reiserfs_inode_info      reiserfs_i;  
                 struct bfs_inode_info           bfs_i;  
                 struct udf_inode_info           udf_i;  
                 struct ncp_inode_info           ncpfs_i;  
                 struct proc_inode_info          proc_i;  
                 struct socket                   socket_i;  
                 struct usbdev_inode_info        usbdev_i;  
                 struct jffs2_inode_info         jffs2_i;  
                 void                            *generic_ip;  
                 } u;  
 }
 //=>目录项:目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计。注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。例如:open一个文件/home/xxx/yyy.txt,那么/、home、xxx、yyy.txt都是一个目录项,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。
//注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。

参考文献

2019-05-29 19:33:01 dark_souls 阅读数 52

1.Linux操作系统环境下常见的开发工具有哪些?

vi编辑器

vi编辑器是Linux和Unix上最基本的文本编辑器,工作在字符模式下。由于不需要图形界面,vi是效率很高的文本编辑器。尽管在Linux上也有很多图形界面的编辑器可用,但vi在系统和服务器管理中的功能是那些图形编辑器所无法比拟的。

GCC编译器

GNU编译器套件(GNU Compiler Collection)包括C、C++、Objective-C、Fortran、Java、Ada和Go语言的前端,也包括了这些语言的库(如libstdc++、libgcj等等)。GCC的初衷是为GNU操作系统专门编写的一款编译器。GNU系统是彻底的自由软件。此处,“自由”的含义是它尊重用户的自由。
gcc编译文件的过程包括以下几个步骤,
1.预处理根据预处理指令所包含的文件内容插入程序中
2.编译:根据预处理文件,调用汇编程序生成汇编代码(.s文件)
3.汇编:调用汇编程序,生成目标文件(.o文件)
4.链接:调用连接器,将程序中用到的函数加到程序中,生成可执行文件

GBD调试工具

Linux中包含一个很强大的调试工具GDB(GNU Debuger),可以用它来调试C和C++程序

以上所有工具,可以百度查询具体用法

2.Linux环境下如何编译调试C语言

编译c语言可以看我这篇文章

用linux编译并运行c文件
https://blog.csdn.net/dark_souls/article/details/89855181

GDB调试看这篇
https://www.cnblogs.com/kooyun/p/5471807.html

3.查看你所使用的Linux操作系统的根目录有哪些目录,并解释他们的作用

linux学习(十二)Linux文件系统目录
https://blog.csdn.net/dark_souls/article/details/90479152

4.Linux操作系统中,文件分访问权限是怎样规定的?如何修改文件的访问权限?

文件的访问权限分为可读(r),可写(w),可执行(x)
修改权限命令为

chmod [-cfvR] [--help] [--version] [who] [+ | - | =] [mode] 文件名

r 读取权限,数字代号为“4”;
w 写入权限,数字代号为“2”;
x 执行或切换权限,数字代号为“1”;
- 不具任何权限,数字代号为“0”;
所以修改文件权限的时候可以直接写数字
例如
chmod 777 file
意思是为file文件赋予可读可写可执行

5.解释inode节点在文件系统中的作用

理解inode,要从文件储存说起。

文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector)。每个扇区储存512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在“块”中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为“索引节点”

6.将U盘连接到usb接口后,如何将其挂载到/mnt/usb目录

首先使用如下命令

fdisk -l

然后创建一个目录:

mkdir -p /mnt/usb
mount -t vfat /dev/sdd1 /mnt/usb

dev/sdd1路径是fdisk -l下查询出来的Device Boot列下的路径
之后就可以通过访问/mnt/usb来访问U盘了
卸载U盘:umount/mnt/usb
卸去U盘: 挂载 Mount /dev/cdrecord /mnt/cdrom
卸载 umount /mnt/cdrom

7.什么是符号链接?什么是硬链接?符合链接和硬链接的区别是什么?

符号链接就是软链接,

软硬链接的区别
https://blog.csdn.net/dark_souls/article/details/90115461

8.建立符号链接文件和硬链接文件之后,如果删除源文件会有什么结果?并且思考原因

删除源文件后硬链接文件还可以使用,但是软链接不可以使用了。
软链接相当于window的快捷方式,保存的是源文件的地址。
硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

9.Linux系统有几种类型的文件?他们分别是什么,有哪些相同点和不同点?

分为四种:1.普通文件2.目录文件3.链接文件4.设备文件
相同点是,它们都是文件,都有一个文件名和i节点号。
不同点是,普通文件的内容为数据,目录文件的内容为目录项或文件名与i节点对应表,而设备文件不占用磁盘空间,
通过其i节点信息可建立与内核驱动程序的联系。
目录文件。文件属主读写执行权限,文件属组读写权限,除属主和属组成员之外的其他用户读权限。
链接文件就是为一个文件起一个或者多个名字

11.假设Linux分配给光驱的设备名是/dev/cdrecord,叙述Linux如何在这个光驱上使用光驱

首先使用如下命令

fdisk -l

然后创建一个目录:

mkdir -p /mnt/usb
mount -t vfat /dev/sdd1 /mnt/usb

dev/sdd1路径是fdisk -l下查询出来的Device Boot列下的路径
之后就可以通过访问/mnt/usb来访问U盘了
卸载U盘:umount/mnt/usb
卸去U盘: 挂载 Mount /dev/cdrecord /mnt/cdrom
卸载 umount /mnt/cdrom

2019-09-03 20:45:28 u013162035 阅读数 61

4.1 udev 设备文件系统

4.1.1什么是Linux设备文件系统

首先我们不看定义,定义总是太抽象很难理解,我们先看现象。当我们往开发板上移植了一个新的文件系统之后(假如各种设备驱动也移植好了),启动开发板,我们用串口工具进入开发板,查看系统/dev目录,往往里面没有或者就只有null、console等几个系统必须的设备文件在这儿外,没有任何设备文件了。那我们移植好的各种设备驱动的设备文件怎么没有啊?如果要使用这些设备,那不是要一个一个的去手动的创建这些设备的设备文件节点,这给我们使用设备带来了极为的不便(在之前篇幅中讲的各种设备驱动的移植都是这样)。

设备文件系统就是给我们解决这一问题的关键,能够在系统设备初始化时动态的在/dev目录下创建好各种设备的设备文件节点(也就是说,系统启动后/dev目录下就有了各种设备的设备文件,直接就可使用了)。除此之外,还可以在设备卸载后自动的删除/dev下对应的设备文件节点(这对于一些热插拔设备很有用,插上的时候自动创建,拔掉的时候又自动删除)。还有一个好处就是,在我们编写设备驱动的时候,不必再去为设备指定主设备号,在设备注册时用0来动态的获取可用的主设备号,然后在驱动中来实现创建和销毁设备文件(一般在驱动模块加载和卸载函数中来实现)。

4.1.2关于udev

2.4 内核使用devfs(设备文件系统)在设备初始化时创建设备文件,设备驱动程序可以指定设备号、所有者、用户空间等信息,devfs 运行在内核环境中,并有不少缺点:可能出现主/辅设备号不够,命名不灵活,不能指定设备名称等问题。

而自2.6 内核开始,引入了sysfs 文件系统。sysfs 把连接在系统上的设备和总线组织成一个分级的文件,并提供给用户空间存取使用。udev 运行在用户模式,而非内核中。udev 的初始化脚本在系统启动时创建设备节点,并且当插入新设备——加入驱动模块——在sysfs上注册新的数据后,udev会创新新的设备节点。

udev 是一个工作在用户空间的工具,它能根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的创建,删除,权限等。这些文件通常都定义在/dev 目录下,但也可以在配置文件中指定。udev 必须内核中的sysfs和tmpfs支持,sysfs 为udev 提供设备入口和uevent 通道,tmpfs 为udev 设备文件提供存放空间。

注意,udev 是通过对内核产生的设备文件修改,或增加别名的方式来达到自定义设备文件的目的。但是,udev 是用户模式程序,其不会更改内核行为。也就是说,内核仍然会创建sda,sdb等设备文件,而udev可根据设备的唯一信息来区分不同的设备,并产生新的设备文件(或链接)。而在用户的应用中,只要使用新产生的设备文件即可。

4.1.3 udev和devfs设备文件的对比

提到udev,不能不提的就是devfs,下面看一下udev 与 devfs 的区别:
1、udev能够实现所有devfs实现的功能。但udev运行在用户模式中,而devfs运行在内核中。

2、当一个并不存在的 /dev 节点被打开的时候, devfs一样自动加载驱动程序而udev确不能。udev设计时,是在设备被发现的时候加载模块,而不是当它被访问的时候。 devfs这个功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生hotplug 事件、加载恰当的驱动,而 udev 将会注意到这点并且为它创建对应的设备节点。如果你不想让所有的设备驱动停留在内存之中,应该使用其它东西来 管理你的模块 (如脚本, modules.conf, 等等) 。其中devfs 用的方法导致了大量无用的 modprobe 尝试,以此程序探测设备是否存在。每个试探性探测都新建一个运行 modprobe 的进程,而几乎所有这些都是无用的

3、udev是通过对内核产生的设备名增加别名的方式来达到上述目的的。前面说过,udev是用户模式程序,不会更改内核的行为。

因此,内核依然会我行我素地产生设备名如sda,sdb等。但是,udev可以根据设备的其他信息如总线(bus),生产商(vendor)等不同来区分不同的设备,并产生设备文件。udev只要为这个设备文件取一个固定的文件名就可以解决这个问题。在后续对设备的操作中,只要引用新的设备名就可以了。但为了保证最大限度的兼容,一般来说,
新设备名总是作为一个对内核自动产生的设备名的符号链接(link)来使用的。
例如:内核产生了sda设备名,而根据信息,这个设备对应于是我的内置硬盘,那我就可以制定udev规则,让udev除了产生/dev/sda设备文件外,另外创建一个符号链接叫/dev/internalHD。这样,我在fstab文件中,就可以用/dev/internalHD来代替原来的 /dev/sda了。下次,由于某些原因,这个硬盘在内核中变成了sdb设备名了,那也不用着急,udev还会自动产生/dev/internalHD这 个链接,并指向正确的/dev/sdb设备。所有其他的文件像fstab等都不用修改。

而在在2.6内核以前一直使用的是 devfs,devfs挂载于/dev目录下,提供了一种类似于文件的方法来管理位于/dev目录下的所有设备,但是devfs文件系统有一些缺点,例 如:不确定的设备映射,有时一个设备映射的设备文件可能不同,例如我的U盘可能对应sda有可能对应sdb 。

4.1.4 udev 的工作流程图

下面先看一张流程图:
在这里插入图片描述

图1

前面提到设备文件系统udev的工作过程依赖于sysfs文件系统。udev文件系统在用户空间工作,它可以根据sysfs文件系统导出的信息(设备号(dev)等),动态建立和删除设备文件。

sysfs文件系统特点:sysfs把连接在系统上的设备和总线组织成为一个分级的目录及文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性,这其中就包括设备的主次设备号。

那么udev 是如何建立设备文件的呢?

a --对于已经编入内核的驱动程序
当被内核检测到的时候,会直接在 sysfs 中注册其对象;对于编译成模块的驱动程序,当模块载入的时候才会这样做。一旦挂载了 sysfs 文件系统(挂载到 /sys),内建的驱动程序在 sysfs 注册的数据就可以被用户空间的进程使用,并提供给 udev 以创建设备节点。

udev 初始化脚本负责在 Linux 启动的时候创建设备节点,该脚本首先将 /sbin/udevsend 注册为热插拔事件处理程序。热插拔事件本不应该在这个阶段发生,注册 udev 只是为了以防万一。

然后 udevstart 遍历 /sys 文件系统(其属性文件dev中记录这设备的主设备号,与次设备号),并在 /dev 目录下创建符合描述的设备文件。

例如,/sys/class/tty/vcs/dev 里含有"7:0"字符串,udevstart 就根据这个字符串创建主设备号为 7 、次设备号为 0 的 /dev/vcs 设备。udevstart 创建的每个设备的名字和权限由 /etc/udev/rules.d/ 目录下的文件指定的规则来设置。如果 udev 找不到所创建设备的权限文件,就将其权限设置为缺省的 660 ,所有者为 root:root 。

b --编译成模块的驱动程序
前面我们提到了"热插拔事件处理程序"的概念,当内核检测到一个新设备连接时,内核会产生一个热插拔事件,并在 /proc/sys/kernel/hotplug 文件里查找处理设备连接的用户空间程序。udev 初始化脚本将 udevsend 注册为该处理程序。当产生热插拔事件的时候,内核让 udev 在 /sys 文件系统里检测与新设备的有关信息,并为新设备在 /dev 里创建项目。

大多数 Linux 发行版通过 /etc/modules.conf 配置文件来处理模块加载,对某个设备节点的访问导致相应的内核模块被加载。对 udev 这个方法就行不通了,因为在模块加载前,设备节点根本不存在。为了解决这个问题,在 LFS-Bootscripts 软件包里加入了 modules 启动脚本,以及 /etc/sysconfig/modules 文件。通过在 modules 文件里添加模块名,就可以在系统启动的时候加载这些模块,这样 udev 就可以检测到设备,并创建相应的设备节点了。如果插入的设备有一个驱动程序模块但是尚未加载,Hotplug 软件包就有用了,它就会响应上述的内核总线驱动热插拔事件并加载相应的模块,为其创建设备节点,这样设备就可以使用了。

4.1.5创建和配置mdev

mdev是udev的简化版本,是busybox中所带的程序,最适合用在嵌入式系统,而udev一般都用在PC上的Linux中,相对mdev来说要复杂些;

1、我们应该明白,不管是udev还是mdev,他们就是一个应用程序,就跟其他应用程序一样(比如:Boa服务),配置了就可以使用了。为了方便起见,我们就使用busybox自带的一个mdev,这样在配置编译busybox时,只要将mdev的支持选项选上,编译后就包含了mdev设备文件系统的应用(当然你也可以不使用busybox自带的,去下载udev的源码进行编译移植。

在这里插入图片描述

图2

2、udev或者mdev需要内核sysfs和tmpfs虚拟文件系统的支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。所以在/etc/fstab配置文件中添加如下内容(红色部分):

在这里插入图片描述

图3

3.、在系统初始化配置文件/etc/init.d/rcS中挂载mdev要用到的sysfs文件系统和tmpfs文件系统,这个在我们介绍根文件系统时已经看到:

在这里插入图片描述

图4

然后启动/sbin目录下的mdev应用对系统的设备进行搜索(红色部分)。

在这里插入图片描述

图5

a – " mdev -s " 的含义是扫描 /sys 中所有的类设备目录,如果在目录中有含有名为“dev”的文件,且文件中包含的是设备号,则mdev就利用这些信息为该设备在/dev下创建设备节点文件;

b – “echo /sbin/mdev > /proc/sys/kernel/hotplug” 的含义是当有热插拔事件产生时,内核就会调用位于 /sbin 目录的 mdev 。这时 mdev 通过环境变量中的 ACTION 和DEVPATH,来确定此次热插拔事件的动作以及影响了 /sys 中的那个目录。接着,会看这个目录中是否有“dev”的属性文件。如果有就利用这些信息为这个设备在 /dev 下创建设备节点文件。

4、在设备驱动程序中加上对类设备接口的支持,即在驱动程序加载和卸载函数中实现设备文件的创建与销毁,这个在我们前面Linux 字符设备驱动结构,自动创建设备节点 可以看到实例,下面在介绍一个

例如在之前篇幅的按键驱动中添加(红色部分):

#include <linux/device.h> //设备类用到的头文件
staticint device_major = DEVICE_MAJOR; //用于保存系统动态生成的主设备号
staticstruct class *button_class; //定义一个类
static int __init button_init(void)
{
    //注册字符设备,这里定义DEVICE_MAJOR=0,让系统去分配,注册成功后将返回动态分配的主设备号
    device_major = register_chrdev(DEVICE_MAJOR, DEVICE_NAME,&buttons_fops);

    if(device_major< 0)
    {
        printk(DEVICE_NAME " register faild!/n");
        return device_major;
    }

    //注册一个设备类,使mdev可以在/dev/目录下建立设备节点
    button_class =class_create(THIS_MODULE, DEVICE_NAME);

    if(IS_ERR(button_class))
    {
        printk(DEVICE_NAME" create class faild!/n");
        return-1;
    }

    //创建一个设备节点,取名为DEVICE_NAME(即my2440_buttons)
    //注意2.6内核较早版本的函数名是class_device_create,现该为device_create
    device_create(button_class,NULL,MKDEV(device_major,0),NULL,DEVICE_NAME);

    return 0;
}

static void __exit button_exit(void)
{
    //注销字符设备
    unregister_chrdev(device_major, DEVICE_NAME);

    //删除设备节点,注意2.6内核较早版本的函数名是class_device_destroy,现该为device_destroy
    device_destroy(button_class,MKDEV(device_major,0));

    //注销类
    class_destroy(button_class);
}

5、至于mdev的配置文件/etc/mdev.conf,这个可有可无,只是设定设备文件的一些规则。我这里就不管他了,让他为空好了。

6、完成以上步骤后,重新编译文件系统,下载到开发板上,启动开发板后进入开发板的/dev目录查看,就会有很多系统设备节点在这里产生了,我们就可以直接使用这些设备节点了。

4.2 sysfs 文件系统与Linux设备模型

提到sysfs 文件系统,必须先需要了解的是Linux设备模型,什么是Linux设备模型呢?

4.2.1 Linux 设备模型

1、设备模型概述
从2.6版本开始,Linux开发团队便为内核建立起一个统一的设备模型。在以前的内核中没有独立的数据结构用来让内核获得系统整体配合的信息。尽管缺乏这些信息,在多数情况下内核还是能正常工作的。然而,随着拓扑结构越来越复杂,以及要支持诸如电源管理等新特性的需求,向新版本的内核明确提出了这样的要求:需要有一个对系统结构的一般性抽象描述,即设备模型。

I 设备、驱动、总线等彼此之间关系错综复杂。如果想让内核运行流畅,那就必须为每个模块编码实现这些功能。如此一来,内核将变得非常臃肿、冗余。而设备模型的理念即是将这些代码抽象成各模块共用的框架,这样不但代码简洁了,也可让设备驱动开发者摆脱这本让人头痛但又必不可少的一劫,将有限的精力放于设备差异性的实现。

II 设备模型用类的思想将具有相似功能的设备放到一起管理,并将相似部分萃取出来,使用一份代码实现。从而使结构更加清晰,简洁。

III 动态分配主从设备号,有效解决设备号的不足。设备模型实现了只有设备在位时才为其分配主从设备号,这与之前版本为每个设备分配一个主从设备号不同,使得有限的资源得到合理利用。

IV 设备模型提供sysfs文件系统,以文件的方式让本是抽象复杂而又无法捉摸的结构清晰可视起来。同时也给用户空间程序配置处于内核空间的设备驱动提供了一个友善的通道。

V 程序具有随意性,同一个功能,不同的人实现的方法和风格各不相同,设备驱动亦是如此。大量的设备亦若实现方法流程均不相同,对以后的管理、重构将是难以想象的工作量。设备模型恰是提供了一个模板,一个被证明过的最优的思路和流程,这减少了开发者设计过程中不必要的错误,也给以后的维护扫除了障碍。

2、设备模型结构
如表,Linux设备模型包含以下四个基本结构:、

表1

在这里插入图片描述
device、driver、bus、class是组成设备模型的基本数据结构。kobject是构成这些基本结构的核心,kset又是相同类型结构kobject的集合。kobject和kset共同组成了sysfs的底层数据体系。本节采用从最小数据结构到最终组成一个大的模型的思路来介绍。当然,阅读时也可先从Device、Driver、Bus、Class的介绍开始,先总体了解设备模型的构成,然后再回到kobject和kset,弄清它们是如何将Device、Driver、Bus、Class穿插链接在一起的,以及如何将这些映像成文件并最终形成一个sysfs文件系统。

4.2.2 sys 文件系统

sysfs是一个基于内存的文件系统,它的作用是将内核信息以文件的方式提供给用户程序使用。
sysfs可以看成与proc,devfs和devpty同类别的文件系统,该文件系统是虚拟的文件系统,可以更方便对系统设备进行管理。它可以产生一个包含所有系统硬件层次视图,与提供进程和状态信息的proc文件系统十分类似。

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

sysfs提供一种机制,使得可以显式的描述内核对象、对象属性及对象间关系。sysfs有两组接口,一组针对内核,用于将设备映射到文件系统中,另一组针对用户程序,用于读取或操作这些设备。表2描述了内核中的sysfs要素及其在用户空间的表现:

表2

在这里插入图片描述

4.2.3深入理解 sysfs 文件系统

sysfs是一个特殊文件系统,并没有一个实际存放文件的介质。
1、kobject结构
sysfs的信息来源是kobject层次结构,读一个sysfs文件,就是动态的从kobject结构提取信息,生成文件。重启后里面的信息当然就没了

sysfs文件系统与kobject结构紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。

Kobject 是Linux 2.6引入的新的设备管理机制,在内核中由struct kobject表示。通过这个数据结构使所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux2.6设备模型的核心结构,Kobject是组成设备模型的基本结构。类似于C++中的基类,它嵌入于更大的对象的对象中,用来描述设备模型的组件。如bus,devices, drivers 等。都是通过kobject连接起来了,形成了一个树状结构。这个树状结构就与/sys向对应。

2、sysfs 如何读写kobject 结构
sysfs就是利用VFS的接口去读写kobject的层次结构,建立起来的文件系统。 kobject的层次结构的注册与注销XX_register()形成的。文件系统是个很模糊广泛的概念, linux把所有的资源都看成是文件,让用户通过一个统一的文件系统操作界面,也就是同一组系统调用,对属于不同文件系统的文件进行操作。这样,就可以对用户程序隐藏各种不同文件系统的实现细节,为用户程序提供了一个统一的,抽象的,虚拟的文件系统界面,这就是所谓"VFS(Virtual Filesystem Switch)"。这个抽象出来的接口就是一组函数操作。

我们要实现一种文件系统就是要实现VFS所定义的一系列接口,file_operations, dentry_operations, inode_operations等,供上层调用。

 file_operations是描述对每个具体文件的操作方法(如:读,写);
 dentry_operations结构体指明了VFS所有目录的操作方法;
 inode_operations提供所有结点的操作方法。

举个例子,我们写C程序,open(“hello.c”, O_RDONLY),它通过系统调用的流程是这样的
open() -> 系统调用-> sys_open() -> filp_open()-> dentry_open() -> file_operations->open()

不同的文件系统,调用不同的file_operations->open(),在sysfs下就是sysfs_open_file()。我们使用不同的文件系统,就是将它们各自的文件信息都抽象到dentry和inode中去。这样对于高层来说,我们就可以不关心底层的实现,我们使用的都是一系列标准的函数调用。这就是VFS的精髓,实际上就是面向对象。

注意sysfs是典型的特殊文件。它存储的信息都是由系统动态的生成的,它动态的包含了整个机器的硬件资源情况。从sysfs读写就相当于向 kobject层次结构提取数据。

下面是详细分析:
a --sysfs_dirent是组成sysfs单元的基本数据结构,它是sysfs文件夹或文件在内存中的代表。sysfs_dirent只表示文件类型(文件夹/普通文件/二进制文件/链接文件)及层级关系,其它信息都保存在对应的inode中。我们创建或删除一个sysfs文件或文件夹事实上只是对以sysfs_dirent为节点的树的节点的添加或删除。sysfs_dirent数据结构如下:

struct sysfs_dirent {
       atomic_t                s_count;
       atomic_t                s_active;
       struct sysfs_dirent  *s_parent;  /* 指向父节点 */
       struct sysfs_dirent  *s_sibling;  /* 指向兄弟节点,兄弟节点是按照inode索引s_ino的大小顺序链接在一起的。*/
       const char             *s_name;    /* 节点名称 */
 
       union {
              struct sysfs_elem_dir            s_dir;      /* 文件夹,s_dir->kobj指向sysfs对象 */
              struct sysfs_elem_symlink    s_symlink;  /* 链接 */
              struct sysfs_elem_attr           s_attr;     /* 普通文件 */
              struct sysfs_elem_bin_attr     s_bin_attr;  /* 二进制文件 */
       };
       unsigned int           s_flags;
ino_t          s_ino;     /* inode索引,创建节点时被动态申请,通过此值和sysfs_dirent地址可以到inode散列表中获取inode结构 */
       umode_t                s_mode;
       struct iattr             *s_iattr;
};

b – inode(index node)中保存了设备的主从设备号、一组文件操作函数和一组inode操作函数。
文件操作比较常见:open、read、write等。inode操作在sysfs文件系统中只针对文件夹实现了两个函数一个是目录下查找inode函数(.lookup=sysfs_lookup),该函数在找不到inode时会创建一个,并用sysfs_init_inode为其赋值;另一个是设置inode属性函数(.setattr=sysfs_setattr),该函数用于修改用户的权限等。inode结构如下:

struct inode {
       struct hlist_node     i_hash;    /* 散列表链节 */
       struct list_head       i_list;
       struct list_head       i_sb_list;
       struct list_head       i_dentry;  /* dentry链节 */
       unsigned long         i_ino;   /* inode索引 */
       atomic_t             i_count;
       unsigned int           i_nlink;
       uid_t                     i_uid;
       gid_t                     i_gid;
       dev_t                    i_rdev; /* 主从设备号 */
const struct inode_operations *i_op; /* 一组inode操作函数,可用其中lookup查找目录下的inode,对应sysfs为sysfs_lookup函数 */
const struct file_operations  *i_fop;    /* 一组文件操作函数,对于sysfs为sysfs的open/read/write等函数 */
       struct super_block  *i_sb;
       struct list_head       i_devices;
       union {
              struct pipe_inode_info    *i_pipe;
              struct block_device       *i_bdev;
              struct cdev            *i_cdev;
       };
};

c --dentry(directory entry)的中文名称是目录项,是Linux文件系统中某个索引节点(inode)的链接。
这个索引节点可以是文件,也可以是目录。引入dentry的目的是加快文件的访问。dentry数据结构如下:

struct dentry {
       atomic_t d_count;         /* 目录项对象使用的计数器 */
       unsigned int d_flags;             /* 目录项标志 */
       spinlock_t d_lock;              /* 目录项自旋锁 */
       int d_mounted;                     /* 对于安装点而言,表示被安装文件系统根项 */
       struct inode *d_inode;           /* 文件索引节点(inode) */
       /*
        * The next three fields are touched by __d_lookup.  Place them here
        * so they all fit in a cache line.
        */
       struct hlist_node d_hash;       /* lookup hash list */
       struct dentry *d_parent; /* parent directory */
       struct qstr d_name;              /* 文件名 */
 
       /*
        * d_child and d_rcu can share memory
        */
       union {
              struct list_head d_child; /* child of parent list */
             struct rcu_head d_rcu;
       } d_u;
      
       void *d_fsdata;                    /* 与文件系统相关的数据,在sysfs中指向sysfs_dirent */
       unsigned char d_iname[DNAME_INLINE_LEN_MIN];      /* 存放短文件名 */
};

sysfs_dirent、inode、dentry三者关系:

在这里插入图片描述

图6

如上图sysfs超级块sysfs_sb、dentry根目录root、sysfs_direct根目录sysfs_root都是在sysfs初始化时创建。

sysfs_root下的子节点是添加设备对象或对象属性时调用sysfs_create_dir/ sysfs_create_file创建的,同时会申请对应的inode的索引号s_ino。注意此时并未创建inode。

inode是在用到的时候调用sysfs_get_inode函数创建并依据sysfs_sb地址和申请到的s_ino索引计算散列表位置放入其中。

dentry的子节点也是需要用的时候才会创建。比如open文件时,会调用path_walk根据路径一层层的查找指定dentry,如果找不到,则创建一个,并调用父dentry的inodelookup函数(sysfs文件系统的为sysfs_lookup)查找对应的子inode填充指定的dentry。

这里有必要介绍一下sysfs_lookup的实现,以保证我们更加清晰地了解这个过程,函数主体如下:

static struct dentry * sysfs_lookup(struct inode *dir, struct dentry *dentry, struct nameidata *nd)
{
       struct dentry *ret = NULL;
       struct sysfs_dirent *parent_sd = dentry->d_parent->d_fsdata; //获取父sysfs_direct
       struct sysfs_dirent *sd;
       struct inode *inode;
 
       mutex_lock(&sysfs_mutex);
      
       /* 在父sysfs_direct查找名为dentry->d_name.name的节点 */
       sd = sysfs_find_dirent(parent_sd, dentry->d_name.name);
 
       /* no such entry */
       if (!sd) {
              ret = ERR_PTR(-ENOENT);
              goto out_unlock;
       }
      
       /* 这儿就是通过sysfs_direct获取对应的inode,sysfs_get_inode实现原理上面已经介绍过了 */
       /* attach dentry and inode */
       inode = sysfs_get_inode(sd);
       if (!inode) {
              ret = ERR_PTR(-ENOMEM);
              goto out_unlock;
       }
      
       /* 填充目录项,至此一个目录项创建完毕 */
       /* instantiate and hash dentry */
dentry->d_op = &sysfs_dentry_ops;   /* 填充目录项的操作方法,该方法只提供一释放inode函数sysfs_d_iput */
       dentry->d_fsdata = sysfs_get(sd);        //填充sysfs_direct
       d_instantiate(dentry, inode);                 //填充inode
       d_rehash(dentry);                               //将dentry加入hash表
 
 out_unlock:
       mutex_unlock(&sysfs_mutex);
       return ret;
}

4.2.4实例分析

a – sysfs文件open流程
open的主要过程是通过指定的路径找到对应的dentry,并从中获取inode,然后获取一个空的file结构,将inode中相关内容赋值给file,这其中包括将inode的fop赋给file的fop。因此接下来调用的filp->fop->open其实就是inode里的fop->open。新的file结构对应一个文件句柄fd,这会作为整个open函数的返回值。之后的read/write操作就靠这个fd找到对应的file结构了。

图7是从网上找到的,清晰地描述了file和dentry以及inode之间的关系

在这里插入图片描述

图7 file、dentry、inode关系

进程每打开一个文件,就会有一个file结构与之对应。同一个进程可以多次打开同一个文件而得到多个不同的file结构,file结构描述了被打开文件的属性,读写的偏移指针等等当前信息。

两个不同的file结构可以对应同一个dentry结构。进程多次打开同一个文件时,对应的只有一个dentry结构。dentry结构存储目录项和对应文件(inode)的信息。

在存储介质中,每个文件对应唯一的inode结点,但是,每个文件又可以有多个文件名。即可以通过不同的文件名访问同一个文件。这里多个文件名对应一个文件的关系在数据结构中表示就是dentry和inode的关系。

inode中不存储文件的名字,它只存储节点号;而dentry则保存有名字和与其对应的节点号,所以就可以通过不同的dentry访问同一个inode。

b – sysfs文件read/write流程
sysfs与普通文件系统的最大差异是,sysfs不会申请任何内存空间来保存文件的内容。事实上再不对文件操作时,文件是不存在的。只有用户读或写文件时,sysfs才会申请一页内存(只有一页),用于保存将要读取的文件信息。如果作读操作,sysfs就会调用文件的父对象(文件夹kobject)的属性处理函数kobject->ktype->sysfs_ops->show,然后通过show函数来调用包含该对象的外层设备(或驱动、总线等)的属性的show函数来获取硬件设备的对应属性值,然后将该值拷贝到用户空间的buff,这样就完成了读操作。写操作也类似,都要进行内核空间ßà用户空间内存的拷贝,以保护内核代码的安全运行。

图为用户空间程序读sysfs文件的处理流程,其他操作类似:

在这里插入图片描述

图8

4.3 VFS 虚拟文件系统解析

4.3.1 VFS 虚拟文件系统基础概念

Linux 允许众多不同的文件系统共存,并支持跨文件系统的文件操作,这是因为有虚拟文件系统的存在。虚拟文件系统,即VFS(Virtual File System)是 Linux 内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如 ext2,vfat 提供接口机制。

Linux 有两个特性:
a --跨文件系统的文件操作
Linux 中允许众多不同的文件系统共存,如 ext2, ext3, vfat 等。通过使用同一套文件 I/O 系统调用即可对 Linux 中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,对文件的操作可以跨文件系统而执行。如图 1 所示,我们可以使用 cp 命令从 vfat 文件系统格式的硬盘拷贝数据到 ext3 文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统。

在这里插入图片描述

图9 跨文件系统的文件操作

b --一切皆是文件
“一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。

在这里插入图片描述

图 10. 一切皆是文件

而虚拟文件系统正是实现上述两点 Linux 特性的关键所在。虚拟文件系统(Virtual File System, 简称 VFS),是 Linux 内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,允许不同的文件系统共存。系统中所有的文件系统不但依赖 VFS 共存,而且也依靠 VFS 协同工作。

为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准的接口,才能与 VFS 协同工作。就像《老炮儿》里面的一样,“要有规矩”,想在Linux下混,就要按照Linux所定的规矩来办事。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。

下图显示了VFS在内核中与实际的文件系统的协同关系。

在这里插入图片描述

图11. VFS在内核中与其他的内核模块的协同关系

总结虚拟文件系统的作用:
虚拟文件系统(VFS)是linux内核和存储设备之间的抽象层,主要有以下好处。

 简化了应用程序的开发:应用通过统一的系统调用访问各种存储介质;
 简化了新文件系统加入内核的过程:新文件系统只要实现VFS的各个接口即可,不需要修改内核部分。

v4.3.2 VFS数据结构

1 、一些基本概念
从本质上讲,文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。

为了描述这个结构,Linux引入了一些基本概念:

 文件 一组在逻辑上具有完整意义的信息项的系列。在Linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待。总之,“一切皆文件”。
 目录 目录好比一个文件夹,用来容纳相关文件。因为目录可以包含子目录,所以目录是可以层层嵌套,形成文件路径。在Linux中,目录也是以一种特殊文件被对待的,所以用于文件的操作同样也可以用在目录上。
 目录项 在一个文件路径中,路径中的每一部分都被称为目录项;如路径/home/source/helloworld.c中,目录 /, home, source和文件 helloworld.c都是一个目录项。
 索引节点 用于存储文件的元数据的一个数据结构。文件的元数据,也就是文件的相关信息,和文件本身是两个不同的概念。它包含的是诸如文件的大小、拥有者、创建时间、磁盘位置等和文件相关的信息。
 超级块 用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。

如上的几个概念在磁盘中的位置关系如图所示。

在这里插入图片描述

图.12 磁盘与文件系统

2、VFS数据结构
VFS依靠四个主要的数据结构和一些辅助的数据结构来描述其结构信息,这些数据结构表现得就像是对象;每个主要对象中都包含由操作函数表构成的操作对象,这些操作对象描述了内核针对这几个主要的对象可以进行的操作。

a – 超级块对象
存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统;每次一个实际的文件系统被安装时,内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。超级块通过其结构中的一个域s_type记录它所属的文件系统类型。超级块的定义在:<linux/fs.h>

/* 
 * 超级块结构中定义的字段非常多,
 * 这里只介绍一些重要的属性
 */
struct super_block {
    struct list_head    s_list;               /* 指向所有超级块的链表 */
    const struct super_operations    *s_op; /* 超级块方法 */
    struct dentry        *s_root;           /* 目录挂载点 */
    struct mutex        s_lock;            /* 超级块信号量 */
    int            s_count;                   /* 超级块引用计数 */

    struct list_head    s_inodes;           /* inode链表 */
    struct mtd_info        *s_mtd;            /* 存储磁盘信息 */
    fmode_t            s_mode;                /* 安装权限 */
};

/*
 * 其中的 s_op 中定义了超级块的操作方法
 * 这里只介绍一些相对重要的函数
 */
struct super_operations {
       struct inode *(*alloc_inode)(struct super_block *sb); /* 创建和初始化一个索引节点对象 */
    void (*destroy_inode)(struct inode *);                /* 释放给定的索引节点 */

       void (*dirty_inode) (struct inode *);                 /* VFS在索引节点被修改时会调用这个函数 */
    int (*write_inode) (struct inode *, int);             /* 将索引节点写入磁盘,wait表示写操作是否需要同步 */
    void (*drop_inode) (struct inode *);                  /* 最后一个指向索引节点的引用被删除后,VFS会调用这个函数 */
    void (*delete_inode) (struct inode *);                /* 从磁盘上删除指定的索引节点 */
    void (*put_super) (struct super_block *);             /* 卸载文件系统时由VFS调用,用来释放超级块 */
    void (*write_super) (struct super_block *);           /* 用给定的超级块更新磁盘上的超级块 */
    int (*sync_fs)(struct super_block *sb, int wait);     /* 使文件系统中的数据与磁盘上的数据同步 */
    int (*statfs) (struct dentry *, struct kstatfs *);    /* VFS调用该函数获取文件系统状态 */
    int (*remount_fs) (struct super_block *, int *, char *); /* 指定新的安装选项重新安装文件系统时,VFS会调用该函数 */
    void (*clear_inode) (struct inode *);                 /* VFS调用该函数释放索引节点,并清空包含相关数据的所有页面 */
    void (*umount_begin) (struct super_block *);          /* VFS调用该函数中断安装操作 */
};

b – 索引节点对象
索引节点对象存储了文件的相关信息,代表了存储设备上的一个实际的物理文件。当一个文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所必需的全部信息;这些信息一部分存储在磁盘特定位置,另外一部分是在加载时动态填充的。索引节点定义在:<linux/fs.h>

/* 
 * 索引节点结构中定义的字段非常多,
 * 这里只介绍一些重要的属性
 */
struct inode {
    struct hlist_node    i_hash;     /* 散列表,用于快速查找inode */
    struct list_head    i_list;        /* 索引节点链表 */
    struct list_head    i_sb_list;  /* 超级块链表超级块  */
    struct list_head    i_dentry;   /* 目录项链表 */
    unsigned long        i_ino;      /* 节点号 */
    atomic_t        i_count;        /* 引用计数 */
    unsigned int        i_nlink;    /* 硬链接数 */
    uid_t            i_uid;          /* 使用者id */
    gid_t            i_gid;          /* 使用组id */
    struct timespec        i_atime;    /* 最后访问时间 */
    struct timespec        i_mtime;    /* 最后修改时间 */
    struct timespec        i_ctime;    /* 最后改变时间 */
    const struct inode_operations    *i_op;  /* 索引节点操作函数 */
    const struct file_operations    *i_fop;    /* 缺省的索引节点操作 */
    struct super_block    *i_sb;              /* 相关的超级块 */
    struct address_space    *i_mapping;     /* 相关的地址映射 */
    struct address_space    i_data;         /* 设备地址映射 */
    unsigned int        i_flags;            /* 文件系统标志 */
    void            *i_private;             /* fs 私有指针 */
};

/*
 * 其中的 i_op 中定义了索引节点的操作方法
 * 这里只介绍一些相对重要的函数
 */
struct inode_operations {
    /* 为dentry对象创造一个新的索引节点 */
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
    /* 在特定文件夹中寻找索引节点,该索引节点要对应于dentry中给出的文件名 */
    struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
    /* 创建硬链接 */
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    /* 从一个符号链接查找它指向的索引节点 */
    void * (*follow_link) (struct dentry *, struct nameidata *);
    /* 在 follow_link调用之后,该函数由VFS调用进行清除工作 */
    void (*put_link) (struct dentry *, struct nameidata *, void *);
    /* 该函数由VFS调用,用于修改文件的大小 */
    void (*truncate) (struct inode *);
};

c – 目录项
和超级块和索引节点不同,目录项并不是实际存在于磁盘上的。在使用的时候在内存中创建目录项对象,其实通过索引节点已经可以定位到指定的文件,但是索引节点对象的属性非常多,在查找,比较文件时,直接用索引节点效率不高,所以引入了目录项的概念。这里可以看做又引入了一个抽象层,目录项是对索引节点的抽象!路径中的每个部分都是一个目录项,比如路径: /mnt/cdrom/foo/bar 其中包含5个目录项,/ mnt cdrom foo bar
每个目录项对象都有3种状态:被使用,未使用和负状态

 被使用:对应一个有效的索引节点,并且该对象由一个或多个使用者
 未使用:对应一个有效的索引节点,但是VFS当前并没有使用这个目录项
 负状态:没有对应的有效索引节点(可能索引节点被删除或者路径不存在了)

目录项定义在:<linux/dcache.h>

/* 目录项对象结构 */
struct dentry {
    atomic_t d_count;       /* 使用计数 */
    unsigned int d_flags;   /* 目录项标识 */
    spinlock_t d_lock;        /* 单目录项锁 */
    int d_mounted;          /* 是否登录点的目录项 */
    struct inode *d_inode;    /* 相关联的索引节点 */
    struct hlist_node d_hash;    /* 散列表 */
    struct dentry *d_parent;    /* 父目录的目录项对象 */
    struct qstr d_name;         /* 目录项名称 */
    struct list_head d_lru;        /* 未使用的链表 */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;    /* child of parent list */
         struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;    /* 子目录链表 */
    struct list_head d_alias;    /* 索引节点别名链表 */
    unsigned long d_time;        /* 重置时间 */
    const struct dentry_operations *d_op; /* 目录项操作相关函数 */
    struct super_block *d_sb;    /* 文件的超级块 */
    void *d_fsdata;            /* 文件系统特有数据 */

    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* 短文件名 */
};

目录项相关操作函数:

struct dentry_operations {
    /* 该函数判断目录项对象是否有效。VFS准备从dcache中使用一个目录项时会调用这个函数 */
    int (*d_revalidate)(struct dentry *, struct nameidata *);
    /* 为目录项对象生成hash值 */
    int (*d_hash) (struct dentry *, struct qstr *);
    /* 比较 qstr 类型的2个文件名 */
    int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
    /* 当目录项对象的 d_count 为0时,VFS调用这个函数 */
    int (*d_delete)(struct dentry *);
    /* 当目录项对象将要被释放时,VFS调用该函数 */
    void (*d_release)(struct dentry *);
    /* 当目录项对象丢失其索引节点时(也就是磁盘索引节点被删除了),VFS会调用该函数 */
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
};

d – 文件对象
文件对象是已打开的文件在内存中的表示,主要用于建立进程和磁盘上的文件的对应关系。

即文件对象并不是一个文件,只是抽象的表示一个打开的文件对。文件对象和物理文件的关系有点像进程和程序的关系一样。

它由sys_open() 现场创建,由sys_close()销毁。当我们站在用户空间来看待VFS,我们像是只需与文件对象打交道,而无须关心超级块,索引节点或目录项。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已经打开的文件,它反过来指向目录项对象(反过来指向索引节点)。

一个文件对应的文件对象可能不是惟一的,但是其对应的索引节点和目录项对象无疑是惟一的。文件对象的定义在: <linux/fs.h>

/* 
 * 文件对象结构中定义的字段非常多,
 * 这里只介绍一些重要的属性
 */
struct file {
    union {
        struct list_head    fu_list;    /* 文件对象链表 */
        struct rcu_head     fu_rcuhead; /* 释放之后的RCU链表 */
    } f_u;
    struct path        f_path;             /* 包含的目录项 */
    const struct file_operations    *f_op; /* 文件操作函数 */
    atomic_long_t        f_count;        /* 文件对象引用计数 */
};

/*
 * 其中的 f_op 中定义了文件对象的操作方法
 * 这里只介绍一些相对重要的函数
 */
struct file_operations {
    /* 用于更新偏移量指针,由系统调用lleek()调用它 */
    loff_t (*llseek) (struct file *, loff_t, int);
    /* 由系统调用read()调用它 */
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    /* 由系统调用write()调用它 */
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    /* 由系统调用 aio_read() 调用它 */
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    /* 由系统调用 aio_write() 调用它 */
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    /* 将给定文件映射到指定的地址空间上,由系统调用 mmap 调用它 */
    int (*mmap) (struct file *, struct vm_area_struct *);
    /* 创建一个新的文件对象,并将它和相应的索引节点对象关联起来 */
    int (*open) (struct inode *, struct file *);
    /* 当已打开文件的引用计数减少时,VFS调用该函数 */
    int (*flush) (struct file *, fl_owner_t id);
};

上面分别介绍了4种对象分别的属性和方法,下面用图来展示这4个对象的和VFS之间关系以及4个对象之间的关系。
VFS中4个主要对象

在这里插入图片描述

图13

前面我们讲到,超级块和索引节点都是真实存在的,是一个实际的物理文件。即使基于内存的文件系统,也是一种抽象的实际物理文件;而目录项对象和文件对象是运行时才被创建的。

下面是VFS中4个主要对象之间的关系:
在这里插入图片描述

图14

超级块是一个文件系统,一个文件系统中可以有多个索引节点;索引节点和目录的关系又是 N:M;一个目录中可以有多个文件对象,但一个文件对象却只能有一个目录;

4.3.3 基于VFS的文件I/O

到目前为止,文章主要都是从理论上来讲述VFS的运行机制;接下来我们将深入源代码层中,通过阐述两个具有代表性的系统调用sys_open()和sys_read()来更好地理解VFS 向具体文件系统提供的接口机制。由于本文更关注的是文件操作的整个流程体制,所以我们在追踪源代码时,对一些细节性的处理不予关心。又由于篇幅所限,只列出相关代码。本文中的源代码来自于linux-2.6.17内核版本。

在深入sys_open()和sys_read()之前,我们先概览下调用sys_read()的上下文。下图描述了从用户空间的read()调用到数据从磁盘读出的整个流程。

当在用户应用程序调用文件I/O read()操作时,系统调用sys_read()被激发,sys_read()找到文件所在的具体文件系统,把控制权传给该文件系统,最后由具体文件系统与物理介质交互,从介质中读出数据。

在这里插入图片描述

图15

1、sys_open()
sys_open()系统调用打开或创建一个文件,成功返回该文件的文件描述符。图8是sys_open()实现代码中主要的函数调用关系图。

在这里插入图片描述

图16 sys_open函数调用关系图

由于sys_open()的代码量大,函数调用关系复杂,以下主要是对该函数做整体的解析;而对其中的一些关键点,则列出其关键代码。
a – 从sys_open()的函数调用关系图可以看到,sys_open()在做了一些简单的参数检验后,就把接力棒传给do_sys_open():

1)、首先,get_unused_fd()得到一个可用的文件描述符;通过该函数,可知文件描述符实质是进程打开文件列表中对应某个文件对象的索引值;
2)、接着,do_filp_open()打开文件,返回一个file对象,代表由该进程打开的一个文件;进程通过这样的一个数据结构对物理文件进行读写操作。
3)、最后,fd_install()建立文件描述符与file对象的联系,以后进程对文件的读写都是通过操纵该文件描述符而进行。

b – do_filp_open()用于打开文件,返回一个file对象;而打开之前需要先找到该文件:
1)、open_namei()用于根据文件路径名查找文件,借助一个持有路径信息的数据结构nameidata而进行;
2)、查找结束后将填充有路径信息的nameidata返回给接下来的函数nameidata_to_filp()从而得到最终的file对象;当达到目的后,nameidata这个数据结构将会马上被释放。

c – open_namei()用于查找一个文件:

1)、path_lookup_open()实现文件的查找功能;要打开的文件若不存在,还需要有一个新建的过程,则调用path_lookup_create(),后者和前者封装的是同一个实际的路径查找函数,只是参数不一样,使它们在处理细节上有所偏差;

2)、当是以新建文件的方式打开文件时,即设置了O_CREAT标识时需要创建一个新的索引节点,代表创建一个文件。在vfs_create()里的一句核心语句dir->i_op->create(dir, dentry, mode, nd)可知它调用了具体的文件系统所提供的创建索引节点的方法。注意:这边的索引节点的概念,还只是位于内存之中,它和磁盘上的物理的索引节点的关系就像位于内存中和位于磁盘中的文件一样。此时新建的索引节点还不能完全标志一个物理文件的成功创建,只有当把索引节点回写到磁盘上才是一个物理文件的真正创建。想想我们以新建的方式打开一个文件,对其读写但最终没有保存而关闭,则位于内存中的索引节点会经历从新建到消失的过程,而磁盘却始终不知道有人曾经想过创建一个文件,这是因为索引节点没有回写的缘故。

3)、path_to_nameidata()填充nameidata数据结构;

4)、may_open()检查是否可以打开该文件;一些文件如链接文件和只有写权限的目录是不能被打开的,先检查nd->dentry->inode所指的文件是否是这一类文件,是的话则错误返回。还有一些文件是不能以TRUNC的方式打开的,若nd->dentry->inode所指的文件属于这一类,则显式地关闭TRUNC标志位。接着如果有以TRUNC方式打开文件的,则更新nd->dentry->inode的信息

d --__path_lookup_intent_open()
不管是path_lookup_open()还是path_lookup_create()最终都是调用__path_lookup_intent_open()来实现查找文件的功能。查找时,在遍历路径的过程中,会逐层地将各个路径组成部分解析成目录项对象,如果此目录项对象在目录项缓存中,则直接从缓存中获得;如果该目录项在缓存中不存在,则进行一次实际的读盘操作,从磁盘中读取该目录项所对应的索引节点。得到索引节点后,则建立索引节点与该目录项的联系。如此循环,直到最终找到目标文件对应的目录项,也就找到了索引节点,而由索引节点找到对应的超级块对象就可知道该文件所在的文件系统的类型。从磁盘中读取该目录项所对应的索引节点;这将引发VFS和实际的文件系统的一次交互。从前面的VFS理论介绍可知,读索引节点方法是由超级块来提供的。而当安装一个实际的文件系统时,在内存中创建的超级块的信息是由一个实际文件系统的相关信息来填充的,这里的相关信息就包括了实际文件系统所定义的超级块的操作函数列表,当然也就包括了读索引节点的具体执行方式。当继续追踪一个实际文件系统ext3的ext3_read_inode()时,可发现这个函数很重要的一个工作就是为不同的文件类型设置不同的索引节点操作函数表和文件操作函数表。

void ext3_read_inode(struct inode * inode)
    {
       ……
       //是普通文件         
       if (S_ISREG(inode->i_mode)) {
          inode->i_op = &ext3_file_inode_operations;
          inode->i_fop = &ext3_file_operations;
          ext3_set_aops(inode);
       } else if (S_ISDIR(inode->i_mode)) {
          //是目录文件
          inode->i_op = &ext3_dir_inode_operations;
          inode->i_fop = &ext3_dir_operations;
       } else if (S_ISLNK(inode->i_mode)) {
          // 是连接文件 
          ……
       } else { 
          // 如果以上三种情况都排除了,则是设备驱动
          //这里的设备还包括套结字、FIFO等伪设备 
          …… 
}

e --nameidata_to_filp子函数:__dentry_open
这是VFS与实际的文件系统联系的一个关键点。从前文分析中可知,调用实际文件系统读取索引节点的方法读取索引节点时,实际文件系统会根据文件的不同类型赋予索引节点不同的文件操作函数集,如普通文件有普通文件对应的一套操作函数,设备文件有设备文件对应的一套操作函数。这样当把对应的索引节点的文件操作函数集赋予文件对象,以后对该文件进行操作时,比如读操作,VFS虽然对各种不同文件都是执行同一个read()操作界面,但是真正读时,内核却知道怎么区分对待不同的文件类型。

static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
					int flags, struct file *f,
					int (*open)(struct inode *, struct file *))
    {
        struct inode *inode;
        ……
        //整个函数的工作在于填充一个file对象
        ……
         f->f_mapping = inode->i_mapping;  
        f->f_dentry = dentry;
        f->f_vfsmnt = mnt;
        f->f_pos = 0;  
        //将对应的索引节点的文件操作函数集赋予文件对象的操作列表
        f->f_op = fops_get(inode->i_fop); 
        ……  
        //若文件自己定义了open操作,则执行这个特定的open操作。
        if (!open && f->f_op)
           open = f->f_op->open; 
        if (open) {
           error = open(inode, f);
           if (error)
              goto cleanup_all;
        ……
        return f;
}

2、 sys_read()
sys_read()系统调用用于从已打开的文件读取数据。如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。图9是sys_read()实现代码中的函数调用关系图。

在这里插入图片描述

图17

对文件进行读操作时,需要先打开它。打开一个文件时,会在内存组装一个文件对象,希望对该文件执行的操作方法已在文件对象设置好。所以对文件进行读操作时,VFS在做了一些简单的转换后(由文件描述符得到其对应的文件对象;其核心思想是返回current->files->fd[fd]所指向的文件对象),就可以通过语句file->f_op->read(file, buf, count, pos)轻松调用实际文件系统的相应方法对文件进行读操作了。

4.3.4解决问题

1、 跨文件系统的文件操作的基本原理
到此,我们也就能够解释在Linux中为什么能够跨文件系统地操作文件了。举个例子,将vfat格式的磁盘上的一个文件a.txt拷贝到ext3格式的磁盘上,命名为b.txt。这包含两个过程,对a.txt进行读操作,对b.txt进行写操作。读写操作前,需要先打开文件。由前面的分析可知,打开文件时,VFS会知道该文件对应的文件系统格式,以后操作该文件时,VFS会调用其对应的实际文件系统的操作方法。

所以,VFS调用vfat的读文件方法将a.txt的数据读入内存;在将a.txt在内存中的数据映射到b.txt对应的内存空间后,VFS调用ext3的写文件方法将b.txt写入磁盘;从而实现了最终的跨文件系统的复制操作。

2、“一切皆是文件”的实现根本
不论是普通的文件,还是特殊的目录、设备等,VFS都将它们同等看待成文件,通过同一套文件操作界面来对它们进行操作。操作文件时需先打开;打开文件时,VFS会知道该文件对应的文件系统格式;当VFS把控制权传给实际的文件系统时,实际的文件系统再做出具体区分,对不同的文件类型执行不同的操作。这也就是“一切皆是文件”的根本所在。

4.3.5总结

VFS即虚拟文件系统是Linux文件系统中的一个抽象软件层;因为它的支持,众多不同的实际文件系统才能在Linux中共存,跨文件系统操作才能实现。
VFS借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向Linux中不管是普通的文件还是目录、设备、套接字等都提供同样的操作界面,如打开、读写、关闭等。只有当把控制权传给实际的文件系统时,实际的文件系统才会做出区分,对不同的文件类型执行不同的操作。由此可见,正是有了VFS的存在,跨文件系统操作才能执行,Unix/Linux中的“一切皆是文件”的口号才能够得以实现。

4.4 proc文件系统

在linux系统中,proc文件系统用于导出内核的信息。proc文件系统一般是只读,也是可以通过编写代码给proc中添加信息。类比window系统的任务管理器。
#cat /proc/meminfo
[root@Linux:/root]# cat /proc/meminfo
MemTotal: 1654540 kB
MemFree: 1626168 kB
Buffers: 916 kB
Cached: 6140 kB
SwapCached: 0 kB
Active: 3428 kB
Inactive: 5392 kB
Active(anon): 1764 kB
Inactive(anon): 0 kB
Active(file): 1664 kB
Inactive(file): 5392 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 1309696 kB
HighFree: 1301500 kB
LowTotal: 344844 kB
LowFree: 324668 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 1788 kB
Mapped: 1356 kB
Shmem: 0 kB
Slab: 4848 kB
SReclaimable: 1908 kB
SUnreclaim: 2940 kB
KernelStack: 432 kB
PageTables: 84 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 827268 kB
Committed_AS: 5284 kB
VmallocTotal: 122880 kB
VmallocUsed: 21596 kB
VmallocChunk: 99196 kB
 常用参数
– MemTotal:总内存
– MemFree:空闲内存
– Cached:缓存
– Active:活跃内存
– Inactive:非活跃内存

#cat /proc/cpuinfo
[root@Linux:/root]# cat /proc/cpuinfo
cpu id : 0xe4412011

Processor : ARMv7 Processor rev 0 (v7l)
processor : 0
BogoMIPS : 1992.29

Features : swp half thumb fastmult vfp edsp neon vfpv3 tls
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x3
CPU part : 0xc09
CPU revision : 0

Hardware : SMDK4X12
Revision : 0000
Serial : 0000000000000000

 常用参数
– cpu id:cpu代号
– Processor:处理器

#cat /proc/interrupts
[root@Linux:/root]# cat /proc/interrupts
CPU0
24: 137 s3c-uart s5pv210-uart
26: 534 s3c-uart s5pv210-uart
98: 0 GIC s3c-pl330.0
99: 0 GIC s3c-pl330.1
100: 0 GIC s3c-pl330.2
107: 0 GIC s3c2410-wdt
108: 0 GIC s3c2410-rtc alarm
121: 9 GIC mct_comp_irq
123: 839 GIC s3c2440-i2c.1
125: 46 GIC s3c2440-i2c.3
126: 60 GIC s3c2440-i2c.4
127: 0 GIC s3c2440-i2c.5
129: 7 GIC s3c2440-i2c.7
134: 15449 GIC ehci_hcd:usb1
135: 1 GIC s3c-udc
139: 39 GIC mmc1
140: 0 GIC mmc2
141: 645 GIC mmc0
142: 0 GIC s3c-csis0
144: 0 GIC s3c-csis1
148: 0 GIC s3c-fimc0
149: 0 GIC s3c-fimc1
150: 0 GIC s3c-fimc2
151: 0 GIC s3c-fimc3
152: 0 GIC s5p-jpeg
153: 0 GIC s5p-fimg2d
155: 0 GIC s5p-mixer
156: 0 GIC s5p-hdmi
158: 0 GIC s3c-mfc
160: 0 GIC samsung-rp
173: 0 GIC samsung-keypad
182: 0 GIC mali_mmu_irq_handlers
183: 0 GIC mali_mmu_irq_handlers
184: 0 GIC mali_mmu_irq_handlers
185: 0 GIC mali_mmu_irq_handlers
186: 0 GIC mali_mmu_irq_handlers
187: 0 GIC mali_core_irq_handlers
188: 0 GIC mali_core_irq_handlers
189: 0 GIC mali_core_irq_handlers
190: 0 GIC mali_core_irq_handlers
191: 0 GIC mali_core_irq_handlers
226: 0 COMBINER s5p-sysmmu.1
227: 0 COMBINER s5p-sysmmu.2
228: 0 COMBINER s5p-sysmmu.3
229: 0 COMBINER s5p-sysmmu.4
230: 0 COMBINER s5p-sysmmu.5
231: 0 COMBINER s5p-sysmmu.15
236: 0 COMBINER s5p-sysmmu.12
237: 0 COMBINER s5p-sysmmu.14
238: 0 COMBINER s5p-sysmmu.13
281: 0 COMBINER s3cfb
352: 1 exynos-eint
359: 0 exynos-eint s3c-sdhci.2
361: 0 exynos-eint BUTTON1
362: 0 exynos-eint BUTTON2
367: 1 exynos-eint s5m87xx-irq
368: 0 exynos-eint BUTTON4
369: 0 exynos-eint BUTTON5
370: 0 exynos-eint switch-gpio
379: 0 exynos-eint BUTTON3
383: 1 exynos-eint hpd
428: 0 s5m8767 rtc-alarm0
IPI0: 0 0 0 0 Timer broadcast interrupts
IPI1: 974 971 300 252 Rescheduling interrupts
IPI2: 2 7 7 4 Function call interrupts
IPI3: 0 1 0 0 Single function call interrupts
IPI4: 0 0 0 0 CPU stop interrupts
IPI5: 0 0 0 0 CPU backtrace
LOC: 6904 1010 417 534 Local timer interrupts
Err: 0

2018-08-14 12:58:54 weixin_37058227 阅读数 1359

 

目录

1.Linux文件系统操作

Linux文件创建,打开,关闭函数

Linux下文件读写函数

2.C库文件操作

3.Linux文件系统

3.1根目录结构

3.2.VFS

VFS 虚拟文件系统基础概念

Linux文件系统与设备驱动关系:

设备驱动结构体:file,inode

inode结构体

inode之atime,mtime,ctime

file结构体

3.3 sysfs文件系统

3.4 udev规则文件


1.Linux文件系统操作

Linux文件创建,打开,关闭函数

#文件权限最终由mode&umask决定
int creat (const char *filename,mode_t mode); //文件创建
int umask(int newmask);                       //修改文件权限
int open (const char *filename,int flags);    //文件打开
int open (const char *filename,int flags,mode_t mode);    //文件打开
int close (int fd);//fd=open(......)//文件关闭函数

其中 flag可为以下一个或组合:(当flags含O_CREAT时,open函数相当于创建文件函数)

mode为以下一个或组合:

当然mode也可以用数字表示,方法如下:

权限数字含义:

数字含义 1 2 4 0
  执行权限 写权限 读权限 无权限

文件权限各位含义(10705为例):

位数 第一位 第二位 第三位 第四位 第五位
作用 设置用户ID 设置组ID 用户权限 组权限 其他人权限
  1 0 7(1+2+4) 0 5(1+4)
  设置 不设置 执行;读;写 无权限 读;执行 

因此10705等价与S_IRWXU|S_IROTH|SIXOTH|S_ISUID

文件打开后,就要对文件进行读写

Linux下文件读写函数

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

指定位置读写:

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

lseek()将文件读写指针相对whence移动offset个字节,函数返回文件指针相对头问价的偏移量

SEEK_SET 相对文件开头
SEEK_CUR 相对文件读写指针的当前位置
SEEK_END 相对文件末尾

2.C库文件操作

利用标准C语言库进行文件读写

3.Linux文件系统

3.1根目录结构

目录

应放置档案内容

/bin

系统有很多放置执行档的目录,但/bin用于存放基本命令,单用户模式下也能运行.

/boot

主要放置开机会使用到的档案,包括Linux核心档案以及开机选单与开机所需设定档等等。Linux kernel常用的档名为:vmlinuz ,如果使用的是grub这个开机管理程式,则还会存在/boot/grub/这个目录。

/dev

设备文件存储目录,应用程序通过对此目录下文件读写和控制以访问实际设备

/etc

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

/home

这是系统预设的使用者家目录(home directory)。 在你新增一个一般使用者帐号时,预设的使用者家目录都会规范到这里来。

/lib

系统库文件存放目录,其下的/lib/modules/目录,放置核心相关的模组(驱动程式)。

/media

media是媒体的英文,这个/media底下放置的就是可移除的装置。 包括软碟、光碟、DVD等等装置都暂时挂载于此

/mnt

挂载储存设备的目录

/opt

可选的意思,有些软件被安装在这里

/root

系统管理员(root)的家目录。 因此即使单用户模式下仅仅挂在根目录,也能访问root目录

/sbin

Linux有非常多指令是用来设定系统环境的,这些指令只有root才能够利用来设定系统,其他使用者最多只能用来查询而已。放在/sbin底下的为开机过程中所需要的,里面包括了开机、修复、还原系统所需要的指令。至于某些伺服器软体程式,一般则放置到/usr/sbin/当中。至于本机自行安装的软体所产生的系统执行档(system binary),则放置到/usr/local/sbin/当中了。常见的指令包括:fdisk, fsck, ifconfig, init, mkfs等等。

/srv

srv可以视为service的缩写,是一些网路服务启动之后,这些服务所需要取用的资料目录。 常见的服务例如WWW, FTP等等。 举例来说,WWW伺服器需要的网页资料就可以放置在/srv/www/里面。呵呵,看来平时我们编写的代码应该放到这里了。

/tmp

这是让一般使用者或者是正在执行的程序暂时放置档案的地方。这个目录是任何人都能够存取的,所以你需要定期的清理一下。当然,重要资料不可放置在此目录啊。 因为FHS甚至建议在开机时,应该要将/tmp下的资料都删除。

/proc

这个目录本身是一个虚拟文件系统(virtual filesystem),它存在于内存中。 操作系统运行时,进程以及内核信息(比如CPU,硬盘分区,内存信息等)存放在这里.他放置的资料都是在内存当中,

/sys 2.6以后内核所支持的sysfs文件系统被映射在此处

/tmp

用户运行时产生的临时文件,/tmp用来用来存储临时文件
/usr 系统存放程序的目录,比如用户命令,用户库
/var 这个目录的内容经常变动,比如/var/log 存放系统日志

3.2.VFS

VFS 虚拟文件系统基础概念

        Linux 允许众多不同的文件系统共存,并支持跨文件系统的文件操作,这是因为有虚拟文件系统的存在。虚拟文件系统,即VFS(Virtual File System)是 Linux 内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如 ext2,vfat 提供接口机制。

Linux文件系统与设备驱动关系:

可以看到

字符设备上层没有类似ext2的文件系统,所以访问字符设备的file_operations成员函数直接由字符设备驱动提供

块设备访问方法有两种:

  • 不通过文件系统直接访问裸设备
  • 通过文件系统访问,file_operations的实现位于文件系统,文件系统把针对文件的读写,转换为针对块原始扇区的读写

设备驱动结构体:file,inode

Linux字符设备中的两个重要结构体(file、inode),含源代码

Linux inode理解

inode结构体

       文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。

       文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。

inodo中包括的内容如下:(总之,除了文件名以外的所有文件信息,都存在inode之中。至于为什么没有文件名,下文会有详细解释。)

  1. 文件字节数
  2. 文件拥有者ID
  3. 文件的Group ID
  4. 文件的读写执行权限
  5. 文件的时间戳(ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间)
  6. 链接数,即有多少个文件名指向这个inode
  7. 文件数据block位置

用stat命令查看文件的inode信息,如下:

inode之atime,mtime,ctime

时间戳 解释 备注

Access

(atime)

在终端上用cat、more 、less、grep、sed、 cp 、file 一个文件时,此文件的Access的时间记录都会被更新(空文件例外),纯粹的access是不会影响modify和change,但会受到modify行为的影响。 ls等命令访问后时间也更新

Modify

(mtime)

当更改了一个文件的内容的时候,此文件的modify的时间记录会被更新。用ls -l看到的文件时间是最近一次modify的时间。modify的行为是三个行为中最有影响力的行为,它发生以后,会使文件的access记录与change记录也同时得到更新。对于目录也是如此。 更改数据后更新,但是拷贝,剪切等操作不会更新该时间

change

(ctime)

对一个文件或者目录作mv、chown、chgrp操作后,它的Change时间记录被更新,change时间会受到modify行为的影响。用ls -lc看到的文件时间是最近一次change的时间。 拷贝等操作后更新,mtime,atime更新后,change也会更新

file结构体

        在设备驱动中,这也是个非常重要的数据结构,必须要注意一点,这里的file与用户空间程序中的FILE指针是不同的,用户空间FILE是定义在C库中,从来不会出现在内核中。而struct file,却是内核当中的数据结构,因此,它也不会出现在用户层程序中。

       file结构体指示一个已经打开的文件(设备对应于设备文件),其实系统中的每个打开的文件在内核空间都有一个相应的struct file结构体,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数,直至文件被关闭。如果文件被关闭,内核就会释放相应的数据结构。

     在内核源码中,struct file要么表示为file,或者为filp(意指“file pointer”), 注意区分一点,file指的是struct file本身,而filp是指向这个结构体的指针。

3.3 sysfs文件系统

打开终端,在/sys目录下,用命令tree打印树形目录

可以发现/sys结构可以描述如下:

设备存储在/sys/devices目录下,而诸如总线(bus),类(class)下的设备,实际只是链接,最终 链接到/sysy/devices下的设备

3.4 udev规则文件

udev规则采用行为单位,每一行代表一个规则,每个规则分为一个或多个匹配部分和赋值部分.

匹配关键词包括:

ACTION: 事件行为,例如:add remove
KERNEL: 内核设备名,例如:sda,ttyUSB*
DEVPATH: 设备的devpath路径,
SUBSYSTEM: 设备的子系统名称,例如sda的子系统为block
BUS: 设备在devpath中的总线名称,例如:USB

DRIVER:

设备在devpath中的设备驱动名称,例如ide-cdrom
SYSFS{filename} 设备的devpath路径下,设备的属性文件"filename"中的内容
ENV{key}: 环境变量。在一条规则中,可以设定最多五条环境变量的 匹配键。
PROGRAM: 调用外部命令。
RESULT: 外部命令 PROGRAM 的返回结果。
ATTR: ATTRS - 匹配设备的sysfs属性,或任何双亲设备的sysfs属性  ,例如:ATTR{size},ATTR{address}

注意:

devpath:devpath是指一个设备在sysfs文件系统(/sys)下的相对路径,该路径包含了指定设备的属性文件。udev里的多数命令都是针对devpath操作的。例如:sda的devpath是/block/sda,sda2的devpath是/block/sda/sda2

udev的重要赋值键:

NAME: 在 /dev下产生的设备文件名。只有第一次对某个设备的 NAME 的赋值行为生效,之后匹配的规则再对该设备的 NAME 赋值行为将被忽略。如果没有任何规则对设备的 NAME 赋值,udev 将使用内核设备名称来产生设备文件。
SYMLINK: 为 /dev/下的设备文件产生符号链接。由于 udev 只能为某个设备产生一个设备文件,所以为了不覆盖系统默认的 udev 规则所产生的文件,推荐使用符号链接。
OWNER, GROUP, MODE: 为设备设定权限。

udev 的值和可调用的替换操作符

Linux 用户可以随意地定制 udev 规则文件的值。例如:my_root_disk, my_printer。同时也可以引用下面的替换操作符:
$kernel, %k:设备的内核设备名称,例如:sda、cdrom。
$number, %n:设备的内核号码,例如:sda3 的内核号码是 3。
$devpath, %p:设备的 devpath路径。
$id, %b:设备在 devpath里的 ID 号。
$sysfs{file}, %s{file}:设备的 sysfs里 file 的内容。其实就是设备的属性值。
$env{key}, %E{key}:一个环境变量的值。
$major, %M:设备的 major 号。
$minor %m:设备的 minor 号。
$result, %c:PROGRAM 返回的结果。
$parent, %P:父设备的设备文件名。
$root, %r:udev_root的值,默认是 /dev/。
$tempnode, %N:临时设备名。
%%:符号 % 本身。
$$:符号 $ 本身。

例子:

      添加串口线,根据芯片自动绑定对应串口别名,参考博客USB hub 多usb接口重映射:udev 规则

 

 

 

 

 

 

 

 

 

 

 

2016-07-11 11:46:09 thisway_diy 阅读数 1800

本章目标

l 了解Linux的文件系统层次标准(FHS)

l 了解根文件系统下各目录的作用

l 掌握构建根文件系统的方法:移植Busybox、构造各个目录、文件等

l 掌握制作yaffs、jffs2文件系统映象文件的方法

推广

想了解更多嵌入式知识请移步到

100ask.taobao.com

 

17.1  Linux文件系统概述

17.1.1  Linux文件系统的特点

类似于Windows下的C、D、E等各个盘,Linux系统也可以将磁盘、Flash等存储设备划分为若干个分区,在不同分区存放不同类别的文件。与Windows的C盘类似,Linux一样要在一个分区上存放系统启动所必需的文件,比如内核映象文件(在嵌入式系统中,内核一般单独存放在一个分区中)、内核启动后运行的第一个程序(init)、给用户提供操作界面的shell程序、应用程序所依赖的库等。这些必需的、基本的文件,合称为根文件系统,它们存放在一个分区中。Linux系统启动后首先挂接这个分区──称为挂接(mount)根文件系统。其他分区上所有目录、文件的集合,也称为文件系统,比如我们常说:“挂接硬盘第二个分区”、“挂接硬盘第二个分区上的文件系统”。

Linux中并没有C、D、E等盘符的概念,它以树状结构管理所有目录、文件,其他分区挂接在某个目录上──这个目录被称为挂接点或安装点(mount point),然后就可以通过这个目录来访问这个分区上的文件了。比如根文件系统被挂接在根目录“/”上后,在根目录下就有根文件系统的各个目录、文件:/bin、/sbin、/mnt等;再将其他分区挂接到/mnt目录上, /mnt目录下就有这个分区的各个目录、文件。

在一个分区上存储文件时,需要遵循一定的格式,这种格式称为文件系统类型,比如fat16、fat32、ntfs、ext2、ext3、jffs2、yaffs等。除这些拥有实实在在的存储分区的文件系统类型外,Linux还有几种虚拟的文件系统类型,比如proc、sysfs等,它们的文件并不存储在实际的设备上,而是在访问它们时由内核临时生成。比如proc文件系统下的uptime文件,读取它时可以得到两个时间值(用来表示系统启动后运行的时间秒数、空闲的时间秒数),每次读取时都由内核即刻生成,每次读取结果都不一样。

“文件系统类型”常被简称为“文件系统”,比如“硬盘第二个分区上的文件系统是EXT2”──这时指的就是文件系统类型。所以“文件系统”这个术语,有时候指的是分区上的文件集合,有时候指的是文件系统类型,需要根据语境分辨,读者在阅读各类文献时需要注意这点。

 

17.1.2  Linux根文件系统目录结构

为了在安装软件时能够预知文件、目录的存放位置,为了让用户方便地找到不同类型的文件,在构造文件系统时,建议遵循FHS标准(Filesystem Hierarchy Standard,文件系统层次标准)。它定义了文件系统中目录、文件分类存放的原则、定义了系统运行所需的最小文件、目录的集合,并列举了不遵循这些原则的例外情况及其原因。FHS并不是一个强制的标准,但是大多的Linux、Unix发行版本遵循FHS。

本节根据FHS标准描述Linux根文件系统的目录结构,并不深入描述各个子目录的结构,读者可以自行阅读FHS标准了解这些内容。FHS文档可以从网站http://www.pathname.com/fhs/中下载。

Linux根文件系统中一般有如图17.1所示的几个目录。

 

图17.1 Linux根文件系统结构

下面依次讲述这几个目录的作用。

1. /bin目录

该目录下存放所有用户(包括系统管理员和一般用户)都可以使用的、基本的命令,这些命令在挂接其他文件系统之前就可以使用,所以/bin目录必须和根文件系统在同一个分区中。

/bin目录下常用的命令有:cat、chgrp、chmod、cp、ls、sh、kill、mount、umount、mkdir、mknod、[、test等。额外说明,[命令其实就是test命令,在脚本文件中“[ expr ]”就等价于“test expr”。

2. /sbin目录

该目录下存放系统命令,即只有管理员能够使用的命令,系统命令还可以存放在/usr/sbin、/usr/local/sbin目录下。/sbin目录中存放的是基本的系统命令,它们用于启动系统、修复系统等。与/bin目录相似,在挂接其他文件系统之前就可以使用/sbin,所以/sbin目录必须和根文件系统在同一个分区中。

/sbin目录下常用的命令有:shutdown、reboot、fdisk、fsck等。

不是急迫需要使用的系统命令存放在/usr/sbin目录下。本地安装的(Locally-installed)的系统命令存放在/usr/local/sbin目录下。

3. /dev目录

该目录下存放的是设备文件。设备文件是Linux中特有的文件类型,在Linux系统下,以文件的方式访问各种外设,即通过读写某个设备文件操作某个具体硬件。比如通过“/dev/ttySAC0”文件可以操作串口0,通过“/dev/mtdblock1”可以访问MTD设备(NAND Flash、NOR Flash等)的第2个分区。

设备文件有两种:字符设备和块设备。在PC上执行命令“ls /dev/ttySAC0 /dev/hda1 -l”可以看到如下结果。其中首行的字母“b”、“c”表示这是一个块设备文件或字符设备文件;“3,   1”、“4,  64”表示设备文件的主、次设备号;主设备号用来表示这是哪类设备,次设备号用来表示这是这类设备中的哪个。

brwxrwxr-x    1 root     49         3,   1 Oct  9  2005 /dev/hda1

crwxrwxr-x    1 root     root       4,  64 Sep 24  2007 /dev/ttySAC0

 

设备文件可以使用mknod命令创建,比如:

mknod /dev/ttySAC0 c 4 64

mknod /dev/hda1 b 3 1

 

/dev的创建有3种方法:

(1)手动创建。

在制作根文件系统的时候,就在/dev目录下创建好要使用的设备文件,比如ttySAC0等。系统挂接根文件系统后,就可以使用/dev目录下的设备文件了。

(2)使用devfs文件系统:这种方法已经过时

在以前的内核中,有一个配置选项CONFIG_DEVFS_FS,它用来将虚拟文件系统devfs挂接在/dev目录上,各个驱动程序注册时会在/dev目录下自动生成各种设备文件。这就免去了手动创建设备文件的麻烦,在制作根文件系统时,/dev目录可以为空。

使用devfs比手动创建设备节点带来很多便利,但是它仍有一些无法克服的缺点,比如:

① 不确定的设备映射:

比如USB接口连接两台打印机A和B,在都开机的情况下以/dev/usb/lp0访问A、以/dev/usb/lp1访问B。但是假如A没有上电,则系统启动时会根据扫描到的设备的顺序,以/dev/usb/lp0访问B。

② 没有足够的主/次设备号:

主次设备号是两个8位的数字,它们并不足以与日益增加的外设一一对应。

③ 命名不够灵活:

由于devfs由内核创建设备节点,当想重新修改某个设备的名字时需要修改、编译内核。

④ devfs消耗大量的内存

由于这些缺点,在linux 2.3.46引入devfs之后,又在linux 2.6.13后面的版本中移除了devfs,而使用udev机制代替。

 

(3)udev。

udev是个用户程序(u 是指user space,dev是指device),它能够根据系统中硬件设备的状态动态地更新设备文件,包括设备文件的创建,删除等。

使用udev机制也不需要在/dev目录下创建设备节点,它需要一些用户程序的支持,并且内核要支持sysfs文件系统。它的操作相对复杂,但是灵活性很高。

在busybox中有一个mdev命令,它是udev命令的简化版本。

 

4. /etc目录

如表17.1、17.2所示,该目录下存放各种配置文件。对于PC上的Linux系统,/etc目录下目录、文件非常多,比如下面两个表格所列出来的。这些目录、文件都是可选的,它们依赖于系统中所拥有的应用程序,依赖于这些程序是否需要配置文件。在嵌入系统中,这些内容可以大为精减。

17.1                       

/etc目录下的子目录

目录 描述
opt 用来配置/opt下的程序(可选)
X11

用来配置X Window(可选)

sgml


用来配置SGML(可选)



xml


用来配置XML(可选)




17.2    

 /etc目录下的文件

文件 描述
export 用来配置NFS文件系统(可选)
fstab 用来指明当执行“mount -a”时,需要挂接的文件系统(可选)
mtab 用来显示已经加载的文件系统,通常是/proc/mounts的链接文件(可选)
ftpusers 启动FTP服务时,用来配置用户的访问权限(可选)
group 用户的组文件(可选)
inittab init进程的配置文件(可选)


 
ld.so.conf
其他共享库的路径(可选)
passwd 密码文件(可选)


5. /lib
目录 

该目录下存放共享库和可加载模块(即驱动程序),其中的共享库用于启动系统、运行根文件系统中的可执行程序,比如/bin、/sbin目录下的程序。其他不是根文件系统所必需的库文件可以放在其他目录,比如/usr/lib、/usr/X11R6/lib、/var/lib等。

表17.3是/lib目录中的内容。

17.3                           

/lib目录中的内容

目录/文件 描述
libc.so.* 动态连接C库(可选)
ld* 连接器、加载器(可选)
modules 内核可加载模式存放的目录(可选)


6. /home
目录 

用户目录,它是可选的。对于每个普通用户,在/home目录下都有一个以用户名命名的子目录,里面存放用户相关的配置文件。

 

7. /root目录

根用户(用户名为root)的目录,与此对应,普通用户的目录是/home下的某个子目录。

 

8. /usr目录

/usr目录的内容可以存在另一个分区中,在系统启动后再挂接到根文件系统中的/usr目录下。里面存放的是共享的、只读的程序和数据,这表明/usr目录下的内容可以在多个主机间共享──这要这些主机也是符合FHS标准的,/usr中的文件应该是只读的,其他主机相关的、可变的文件应该保存在其他目录下,比如/var。

/usr目录通常包含如下内容,嵌入式系统中,这些内容可以进一步精减。/usr目录中的内容如表17.4所示。

17.4                              

/usr目录中的内容

目录 描述
bin 很多用户命令存放在这个目录下
include C程序的头文件,这在PC上进行开发时才用到,在嵌入式系统中不需要
lib 库文件
local 本地目录
sbin 非必需的系统命令(必需的系统命令放在/sbin目录下)
share 架构无关的数据
X11R6 XWindow系统
games 游戏
src 源代码


9. /var
目录 

与/usr目录相反,/var目录中存放可变的数据,比如spool目录(mail、news、打印机等用的), log文件、临时文件。

 

10. /proc目录

这是一个空目录,常作为proc文件系统的挂接点。proc文件系统是个虚拟的文件系统,它没有实际的存储设备,里面的目录、文件都是由内核临时生成的,用来表示系统的运行状态,也可以操作其中的文件控制系统。

系统启动后,使用以下命令挂接proc文件系统(常在/etc/fstab进行设置以自动挂接):

# mount –t proc none /proc

 

11. /mnt目录

用于临时挂接某个文件系统的挂接点,通常是空目录;也可以在里面创建一些空的子目录,比如/mnt/cdram、/mnt/hda1等,用来临时挂接光盘、硬盘。

 

12. /tmp目录

用于存放临时文件,通常是空目录。一些需要生成临时文件的程序要用到/tmp目录,所以/tmp目录必须存在并可以访问。

为减少对Flash的操作,当在/tmp目录上挂接内存文件系统,如下:

# mount –t tmpfs none /tmp

 

 

17.1.3  Linux文件属性介绍

Linux系统有如表17.5所示的几种文件类型。

17.5                             

Linux文件类型

文件类型 描述
普通文件 这是最常见的文件类型
目录文件 目录也是一种文件
字符设备文件 用来访问字符设备
块设备文件 用来访问块设备
FIFO 用于进程间的通信,也称为命名管道
套接口 用于进程间的网络通信
连接文件 它指向另一个文件,有软连接、硬连接


使用“ls -lih”命令可以看到各个文件的具体信息,下面选取这几种文件,列出它们的信息:
 

228883 -rw-r--r--    2 root     root            6 Sep 27 22:10 readme.txt

228884 lrwxrwxrwx    1 root     root           10 Sep 27 22:11 ln_soft -> readme.txt

 228883 -rw-r--r--    2 root     root            6 Sep 27 22:10 ln_hard

 228882 drwxr-xr-x    2 root     root         4.0K Sep 27 22:10 tmp_dir

 228880 crw-r--r--    1 root     root       4,  64 Sep 27 22:09 ttySAC0

 228881 brw-r--r--    1 root     root      31,   0 Sep 27 22:09 mtdblock0

 228885 prw-r--r--    1 root     root            0 Sep 27 22:16 my_fifo

343929 srwxr-xr-x    1 root     root            0 May 20  2006 klaunchertIdhOa.slave-socket

除设备文件ttySAC0、mtdblock0外,这些信息都分为8个字段,比如:

228883 -rw-r--r--    2 root     root            6 Sep 27 22:10 readme.txt

字段1     2         3  4        5              6      7          8

它们的意义如下:

(1)字段1:文件的索引节点inode

索引节点里存放一个文件的上述信息,比如文件大小、属主、归属的用户组、读写权限等,并指明文件的实际数据存放的位置。

(2)字段2:文件种类和权限

这字段共分10位,格式如下:

图17.2 文件类型及属性


文件类型有7种,“-”表示普通文件,“d”表示目录,“c”表示字符设备,“b”表示块设备,“p”表示FIFO(即管道),“l”表示软连接(也称符号连接),“s”表示套接口(socket)。

没有专门的符号来表示“硬连接”类型,硬连接也是普通文件,只不过文件的实际内容只有一个副本,连接文件、被连接文件都指向它。比如上面的ln_hard文件是使用命令“ln readme.txt ln_hard”创建出来的到readme.txt文件的硬连接,readme.txt和ln_hard的地位完全一致,它们都指向文件系统中的同一个位置,它们的“硬连接个数”都是2,表示这个文件的实际内容被引用两次──可以从上面的文件信息中看到这两个文件的inode都是228883。

硬连接文件的引入作用有二:使得可以用别名来引用一个文件,避免文件被误删除──只有当硬连接个数为1时,对一个文件执行删除操作才会真正删除文件的副本。但是它有如下缺点:不能创建到目录的连接,被连接文件和连接文件必须在同一个文件系统中。对此,引入软连接,也称符号连接,软连接只是简单地指向一个文件(可以是目录),并不增加它的硬连接个数。比如上面的ln_soft文件就是使用命令“ln -s readme.txt ln_hard”创建出来的到readme.txt文件的软连接,它使用另一个inode。

剩下的9位分为3组,分别用来表示文件拥有者、同一个群组的用户、其他用户对这个文件的访问权限。每组权限由rwx三位组成,表示可读、可写、可执行。如果某一位被设为“-”,则表示没有相应的权限,比如“rw-”表示只有读写权限,没有执行权限。

(3)字段3:硬连接个数,这在上面已经提到。

(4)字段4:文件拥有者

(5)字段5:所属群组

(6)字段6:文件或目录的大小

(7)字段7:最后访问或修改时间

(8)字段8:文件名或目录名

 

对于设备文件,字段6表示主设备号,字段7表示次设备号。

 

17.2  移植Busybox

所谓制作根文件系统,就是创建上节提到的各种目录,并且在里面创建各种文件。比如在/bin、/sbin目录下存放各种可执行程序,在/etc目录下存放配置文件,在/lib目录下存放库文件。这节讲述如何使用Busybox来创建/bin、/sbin等目录下的可执行文件。

17.2.1  Busybox概述

Busybox是一个遵循GPL v2协议的开源项目。Busybox将众多的UNIX命令集合进一个很小的可执行程序中,可以用来替换GNU fileutils、shellutils等工具集。Busybox中各种命令与相应的GNU工具相比,所能提供的选项较少,但是能够满足一般应用。Busybox为各种小型的或者嵌入式系统提供了一个比较完全的工具集。

Busybox在编写过程对文件大小进行的优化,并考虑了系统资源有限(比如内存等)的情况。与一般的GNU工具集动辄几M的体积相比,动态连接的Busybox只有几百K,即使静态连接也只有1M左右。Busybox按模块进行设计,可以很容易地加入、去除某些命令,或增减命令的某些选项。

在创建一个最小的根文件系统时,使用Busybox的话,只需要在/dev目录下创建必要的设备节点、在/etc目录下创建一些配置文件就可以了──当然,如果Busybox使用动态连接,还要在/lib目录下包含库文件。

Busybox支持uClibc库和glibc库,对Linux 2.2.x之后的内核支持良好。

Busybox的官方网站是http://www.busybox.net/,源码可以从http://www.busybox.net/downloads/下载,本书使用busybox-1.7.0.tar.bz2。

 

17.2.2  init进程介绍及用户程序启动过程

本节介绍Linux系统中用户程序启动的一些基础知识,读者可以直接阅读下一节开始移植Busybox。

init进程是由内核启动的第一个(也是唯一的一个)用户进程(进程ID为1),它根据配置文件决定启动哪些程序,比如执行某些脚本、启动shell、运行用户指定的程序等。init进程是后续所有进程的发起者,比如init进程启动/bin/sh程序后,才能够在控制台上输入各种命令。

init进程的执行程序通常是/sbin/init,上面讲述的init进程的作用只不过是/sbin/init这个程序的功能。我们完全可以编写自己的/sbin/init程序,或者传入命令行参数“init=xxxxx”指定某个程序作为init进程运行。

一般而言,在Linux系统有两种init程序:BSD init和System V init。BSD和System V是两种版本的UNIX系统。这两种init程序各有优缺点,现在大多Linux的发行版本使用System V init。但是在嵌入式领域,通常使用Busybox集成的init程序,下面基于它进行讲解。

1. 内核如何启动init进程

内核启动的最后一步就是启动init进程,代码在init/main.c文件中:

748 static int noinline init_post(void)

749 {

……

756     if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)

757         printk(KERN_WARNING "Warning: unable to open an initial console.\n");

758

759     (void) sys_dup(0);

760     (void) sys_dup(0);

761

762     if (ramdisk_execute_command) {

763         run_init_process(ramdisk_execute_command);

764         printk(KERN_WARNING "Failed to execute %s\n",

765                 ramdisk_execute_command);

766     }

……

774     if (execute_command) {

775         run_init_process(execute_command);

776         printk(KERN_WARNING "Failed to execute %s.  Attempting "

777                     "defaults...\n", execute_command);

778     }

779     run_init_process("/sbin/init");

780     run_init_process("/etc/init");

781     run_init_process("/bin/init");

782     run_init_process("/bin/sh");

783

784     panic("No init found.  Try passing init= option to kernel.");

785 }

786

代码并不复杂,其中的run_init_process函数使用它的参数所指定的程序来创建一个用户进程。需要注意,一旦run_init_process函数创建进程成功,它将不会返回。

内核启动init进程的过程如下:

(1) 打开标准输入、标准输出、标准错误设备。

Linux中最先打开的3个文件分别称为标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们对应的文件描述符分别为0、1、2。所谓标准输入就是在程序中使用scanf(……)、fscanf(stdin, ……)获取数据时,从哪个文件(设备)读取数据;标准输出、标准错误都是输出设备,前者对应printf(……)、fprintf(stdout, ……),后者对应fprintf(stderr, ……)。

第756行尝试打开/dev/console设备文件,如果成功,它就是init进程标准输入设备。

第759、760将文件描述符0复制给文件描述符1、2,所以标准输入、标准输出、标准错误都对应同一个文件(设备)。

在移植Linux内核时,如果发现打印出“Warning: unable to open an initial console.”,其原因大多是:根文件系统虽然被正确挂接了,但是里面的内容不正确──要么没有/dev/console这个文件,要么它没有对应的设备。

 

(2) 如果ramdisk_execute_command变量指定了要运行的程序,启动它。

ramdisk_execute_command的取值(代码也在init/main.c中)分3种情况:

① 如果命令行参数中指定了“rdinit=……”,则ramdisk_execute_command等于这个参数指定的程序。

② 否则,如果/init程序存在,ramdisk_execute_command就等于“/init”。

③ 否则,ramdisk_execute_command为空。

本书所用的命令行没有设定“rdinit=……”,根文件系统中也没有/init程序,所以ramdisk_execute_command为空,第763~765这几行的代码不执行。

 

(3) 如果execute_command变量指定了要运行的程序,启动它。

如果命令行参数中指定了“init=……”,则execute_command等于这个参数指定的程序,否则为空。

本书所用的命令行没有设定“init=……”,所以第775~777这几行的代码不执行。

 

(4) 依次尝试执行/sbin/init、/etc/init、/bin/init、/bin/sh。

第779行执行/sbin/init程序,这个程序在我们的根文件系统中是存在的,所以init进程所用的程序就是/sbin/init。从此系统的控制权交给/sbin/init,不再返回init_post函数中。

run_init_process函数也在init/main.c中,代码如下:

184 static char * argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };

185 char * envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, };

……

739 static void run_init_process(char *init_filename)

740 {

741     argv_init[0] = init_filename;

742     kernel_execve(init_filename, argv_init, envp_init);

743 }

744

所以执行/sbin/init程序时,它的环境参数为“"HOME=/", "TERM=linux"”。

 

2. Busybox init进程的启动过程

Busybox init程序对应的代码在init/init.c文件中,下面以busybox-1.7.0为例进行讲解。

先概述其流程,再结合一个/etc/inittab文件讲述init进程的启动过程。

(1)Busybox init程序流程。

流程图如图17.3所示,其中与构建根文件系统关系密切的是控制台的初始化、对inittab文件的解释及执行。

图17.3 Busybox init程序流程图

 

内核启动init进程时已经打开“/dev/console”设备作为控制台,一般情况下Busybox init程序就使用/dev/console。但是如果内核启动init进程的同时设置了环境变量CONSOLE或console,则使用环境变量所指定的设备。在Busybox init程序中,还会检查这个设备是否可以打开,如果不能打开则使用“/dev/null”。

Busybox init进程只是作为其他进程的发起者和控制者,并不需要控制台与用户交互,所以init进程会把它关掉──系统启动后运行命令“ls /proc/1/fd/”可以看到该目录为空。init进程创建其他子进程时,如果没有在/etc/inittab中指明它的控制台,则使用前面确定的控制台。

 

/etc/inittab文件的相关文档和示例代码都在Busybox的examples/inittab文件中。

如果存在/etc/inittab文件,Busybox init程序解析它,然后按照它的指示创建各种子进程;否则使用默认的配置创建子进程。

/etc/inittab文件中每个条目用来定义一个子进程,并确定它的启动方法。格式如下:

<id>:<runlevels>:<action>:<process>

例如:

ttySAC0::askfirst:-/bin/sh

对于Busybox init程序,上述各个字段作用如下:

① <id>:表示这个子进程要使用的控制台(即标准输入、标准输出、标准错误设备)。如果省略,则使用与init进程一样的控制台。

② <runlevels>:对于Busybox init程序,这个字段没有意义,可以省略。

③ <action>:

表示init进程如何控制这个子进程,有如表17.6所示的8种取值:

17.6                  /etc/inittab文件中<action>字段的意义

action名称 执行条件 说明
sysinit 系统启动后最先执行 只执行一次,init进程等待它结束才继续执行其他动作

wait

 

系统执行完sysinit进程后 只执行一次,init进程等待它结束才继续执行其他动作
once 系统执行完wait进程后 只执行一次,init进程不等待它结束
respawn 启动完once进程后 init进程监测发现子进程退出时,重新启动它
askfirst 启动完respawn进程后 与respawn类似,不过init进程先输出“Please press Enter to activate this console.”,等用户输入回车键之后才启动子进程。
shutdown 当系统关机时 即重启、关闭系统命令时
restart Busybox中配置了CONFIG_FEATURE_USE_INITTAB,并且init进程接收到SIGHUP信号时 先重新读取、解析/etc/inittab文件,再执行restart程序
ctrlaltdel 按下Ctrl+Alt+Delete组合键时  


④ <process>:要执行的程序,它可以是可执行程序,也可以是脚本。
 

如果<procss>字段前有“-”字符,这个程序被称为“交互的”。

 

在/etc/inittab文件文件的控制下,init进程的行为总结如下:

① 在系统启动前期,init进程首先启动<action>为sysinit、wait、once的3类子进程。

② 在系统正常运行期间,init进程首先启动<action>为respawn、askfirst的两类子进程,并监视它们,发现某个子进程退出时重新启动它。

③ 在系统退出时,执行<action>为shutdown、restart、ctrlaltdel的3类子进程(之一或全部)。

 

如果根文件系统中没有/etc/inittab文件,Busybox init程序将使用如下默认的inittab条目:

::sysinit:/etc/init.d/rcS

::askfirst:/bin/sh

tty2::askfirst:/bin/sh

tty3::askfirst:/bin/sh

tty4::askfirst:/bin/sh

::ctrlaltdel:/sbin/reboot

::shutdown:/sbin/swapoff -a

::shutdown:/bin/umount -a -r

::restart:/sbin/init

 

(2)/etc/inittab实例。

仿照Busybox的examples/inittab文件,创建一个inittab文件,内容如下:

# /etc/inittab

# 这是init进程启动的第一个子进程,它是一个脚本,可以在里面指定用户想执行的操作

# 比如挂接其他文件系统、配置网络等

::sysinit:/etc/init.d/rcS

 

# 启动shell,以/dev/ttySAC0作为控制台

ttySAC0::askfirst:-/bin/sh

 

# 按下Ctrl+Alt+Delete之后执行的程序,不过在串口控制台中无法输入Ctrl+Alt+Delete组合键

::ctrlaltdel:/sbin/reboot

# 重启、关机前执行的程序

::shutdown:/bin/umount -a -r

 

17.2.3  编译/安装Busybox

http://www.busybox.net/downloads/下载busybox-1.7.0.tar.bz2。

使用如下命令解压得到busybox-1.7.0目录,里面就是所有的源码:

$ tar xjf busybox-1.7.0.tar.bz2

 

Busybox集合了几百个命令,在一般系统中并不需要全部使用。可以通过配置Busybox来选择这些命令、定制某些命令的功能(选项)、指定Busybox的连接方法(动态连接还是静态连接)、指定Busybox的安装路径。

1.  配置Busybox

在busybox-1.7.0目录下执行“make menuconfig”命令即可进入配置界面。Busybox将所有配置项分类存放,表17.7列出了这些类别,其中的“说明”是针对嵌入式系统而言的:

17.7                         Busybox配置选项分类

配置项类型 说明
Busybox Settings Busybox的一些总体设置,里面分为下面5个子类

Busybox Settings --->

General Configuration

一些通用的设置,一般不需要理会

Busybox Settings --->

Build Options

连接方式、编译选项等

Busybox Settings --->

Debugging Options

调试选项,使用Busybox时将打印一些调试信息。一般不选。

Busybox Settings --->

Installation Options

Busybox的安装路径,不需设置,可以在命令行中指定

Busybox Settings --->

Busybox Library Tuning

Busybox的性能微调,比如设置在控制台上可以输入的最大字符个数,一般使用默认值即可
Archival Utilities 各种压缩、解压缩工具,根据需要选择相关命令
Coreutils 核心的命令,比如ls、cp等。
Console Utilities 控制台相关的命令,比如清屏命令clear等。只是提供一些方便而已,可以不理会。
Debian Utilities Debian命令(Debian是Linux的一种发行版本),比如which命令可以用来显示一个命令的完整路径。
Editors 编辑命令,一般都选中vi
Finding Utilities 查找命令,一般不用
Init Utilities init程序的配置选项,比如是否读取inittab文件。使用默认配置即可。
Login/Password Management Utilities 登录、用户帐号/密码等方面的命令
Linux Ext2 FS Progs Ext2文件系统的一些工具
Linux Module Utilities 加载/卸载模块的命令,一般都选中
Linux System Utilities 一些系统命令,比如显示内核打印信息的dmesg命令、分区命令fdisk等。
Miscellaneous Utilities 一些不好分类的命令
Networking Utilities 网络方面的命令,可以选择一些可以方便调试的命令,比如telnetd、ping、tftp等。
Process Utilities 进程相关的命令,比如查看进程状态的命令ps、查看内存使用情况的命令free、发送信号的命令kill、查看最消耗CPU资源的前几个进程的命令top等。为方便调试,可以都选中。
Shells 有多种shell,比如msh、ash等。一般选择ash。
System Logging Utilities 系统记录(log)方面的命令
Runit Utilities 本书没有用到
ipsvd utilities 监听TCP、DPB端口,发现有新的连接时启动某个程序
   

 本节使用默认配置,执行“make menuconfig”后退出、保存配置即可。

下面只讲述一些常用的选项,以便读者参考。Busybox的配置过程大多是选择、去除各种命令,一目了然。

(1)Busybox的性能微调。

设置TAB键补全,比如在控制给上输入一个“ifc”后按TAB键,它会补全为“ifconfig”。如下配置:

Busybox Settings  --->

    Busybox Library Tuning  --->

        [*]   Tab completion

 

(2)连接/编译选项。

以下选项指定是否使用静态连接:

Build Options  --->

    [ ] Build BusyBox as a static binary (no shared libs)

使用glibc时,如果静态编译Buxybox会提示以下警告信息,表示会出现一些莫名其秒的问题:

#warning Static linking against glibc produces buggy executables

所以,本书使用动态连接的Busybos,在构造根文件系统时需要在/lib目录下放置glibc库文件。

 

(3)Archival Utilities选项。

选择tar命令:

Archival Utilities  --->

    [*] tar

    [*]     Enable archive creation

    [*]     Enable -j option to handle .tar.bz2 files

    [*]     Enable -X (exclude from) and -T (include from) options)

    [*]     Enable -z option

    [*]     Enable -Z option

    [*]     Enable support for old tar header format

    [*]     Enable support for some GNU tar extensions

    [*]     Enable long options

 

(4)Linux Module Utilities选项。

要使用可加载模块,下面的配置要选上:

Linux Module Utilities  --->    

    [*] insmod

    [*]   Module version checking

    [*]   Add module symbols to kernel symbol table

    [*]   In kernel memory optimization (uClinux only)

    [*]   Enable load map (-m) option

    [*]     Symbols in load map

    [*] rmmod

    [*] lsmod

    [*] Support version 2.6.x Linux kernels

(5)Linux System Utilities选项。

支持mdev,这可以很方便地构造/dev目录,并且可以支持热拔插设备。另外,为方便调试,选中mount、umount命令,并让mount命令支持NFS(网络文件系统)。

Linux System Utilities  --->

    [*] mdev

    [*]   Support /etc/mdev.conf

    [*]     Support command execution at device addition/removal

    [*] mount

    [*]     Support mounting NFS file systems

    [*] umount

    [*]   umount -a option

 

(6)Networking Utilities选项。

除其他默认配置外,增加ifconfig命令:

Networking Utilities  --->

    [*] ifconfig

    [*]     Enable status reporting output (+7k)

    [ ]     Enable slip-specific options "keepalive" and "outfill"

    [ ]     Enable options "mem_start", "io_addr", and "irq"

    [*]     Enable option "hw" (ether only)

    [*]     Set the broadcast automatically

 

2.  编译和安装Busybox

编译之前,先修改Busybox根目录的Makefile,使用交叉编译器:

175 ARCH            ?= $(SUBARCH)

176 CROSS_COMPILE   ?=

改为:

175 ARCH            ?= arm

176 CROSS_COMPILE   ?= arm-linux-

 

然后可执行“make”命令编译Busybox。

最后是安装,执行“make CONFIG_PREFIX=dir_path install”就可以将Busybox安装在dir_name指定的目录下。执行以下命令在/work/nfs_root/fs_mini目录下安装Busybox:

$ make CONFIG_PREFIX=/work/nfs_root/fs_mini install

 

一切完成后,将在/work/nfs_root/fs_mini目录下生成如下文件、目录:

drwxr-xr-x 2 book book 4096 2008-01-22 06:56 bin

lrwxrwxrwx 1 book book   11 2008-01-22 06:56 linuxrc -> bin/busybox

drwxr-xr-x 2 book book 4096 2008-01-22 06:56 sbin

drwxr-xr-x 4 book book 4096 2008-01-22 06:56 usr

 

其中linuxrc和上面分析的/sbin/init程序功能完全一样;其他目录下是各种命令,不过它们都是到/bin/busybox的符号连接,比如/work/nfs_root/fs_mini/sbin目录下:

lrwxrwxrwx 1 book book 14 2008-01-22 06:56 halt -> ../bin/busybox

lrwxrwxrwx 1 book book 14 2008-01-22 06:56 ifconfig -> ../bin/busybox

lrwxrwxrwx 1 book book 14 2008-01-22 06:56 init -> ../bin/busybox

lrwxrwxrwx 1 book book 14 2008-01-22 06:56 insmod -> ../bin/busybox

lrwxrwxrwx 1 book book 14 2008-01-22 06:56 klogd -> ../bin/busybox

……

除bin/busybox外,其他文件都是到bin/busybox的符号连接──busybox是所有命令的集合体,这些符号连接文件可以直接运行。比如在开发板上,运行“ls”命令和“busybox ls”命令是一样的。

 

 

17.3  使用glibc

在第二章制作交叉编译工具链时,已经生成了glibc库,可以直接使用它来构建根文件系统。

17.3.1  glibc库的组成

第二章制作的交叉编译工具链中,glibc库的位置是/work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib。

需要澄清一点,这个目录下的文件并非都属于glibc库,比如crt1.o、libstdc++.a等文件是GCC工具本身生成的。本书不区分它们的来源,统一处理。

里面的目录、文件可以分为8类:

① 加载器ld-2.3.6.so、ld-linux.so.2:

动态程序启动前,它们被用来加载动态库。

② 目标文件(.o):

比如crt1.o、crti.o、crtn.o、gcrt1.o、Mcrt1.o、Scrt1.o等。在生成应用程序时,这些文件像一般的目标文件一样被连接。

③ 静态库文件(.a):

比如静态数学库libm.a、静态c++库libstdc++.a等,编译静态程序时会连接它们。

④ 动态库文件(.so、.so.[0-9]*):

比如动态数学库libm.so、动态c++库libstdc++.so等,它们可能是一个链接文件。编译动态库时会用到这些文件,但是不会连接它们──在运行时才连接。

⑤ libtool库文件(.la):

在连接库文件时,这些文件会被用到,比如它们列出了当前库文件所依赖的其他库文件。程序运行时无需这些文件。

⑥ gconv目录:

里面是有头字符集的动态库,比如ISO8859-1.so、GB18030.so等。

⑦ ldscripts目录:

里面是各种连接脚本,在编译应用程序时,它们被用样指定程序的运行地址、各段的位置等。

⑧ 其他目录及文件

 

17.3.2  安装glibc

在开发板上只需要加载器和动态库,假设要构建的根文件系统目录为/work/nfs_root/fs_mini,如下操作即可:

$ mkdir -p /work/nfs_root/fs_mini/lib

$ cd /work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib

$ cp *.so* /work/nfs_root/fs_mini/lib –d

 

上面复制的库文件不是每个都会被用到,可以根据应用程序对库的依赖关系保留需要用到的。通过ldd命令可以查看一个程序会用到哪些库,主机自带的ldd命令不能查看交叉编译出来的程序,有两种替代方法:

① 如果有uClibc-0.9.28的代码,可以进入utils子目录生成ldd.host工具:

$ cd uClibc-0.9.28/utils

$ make ldd.host

然后将生成的ldd.host放到主机/usr/local/bin目录下即可使用。

比如对于动态连接的Busybox,它的库依赖关系如下:

$ ldd.host busybox

        libcrypt.so.1 => /lib/libcrypt.so.1 (0x00000000)

        libm.so.6 => /lib/libm.so.6 (0x00000000)

        libc.so.6 => /lib/libc.so.6 (0x00000000)

        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00000000)

这表示Busybox要使用的库文件有libcrypt.so.1、libm.so.6、libc.so.6,加载器为/lib/ld-linux.so.2(实际上在交叉工具链目录下,加载器为ld-linux.so.2)。上面的“not found”表示主机上没有这个文件──这没关系,单板的根文件系统上有就行。

 

② 可以使用以下命令:

$ arm-linux-readelf -a "your binary" | grep "Shared"

 

比如对于动态连接的Busybox,它的库依赖关系如下:

$ arm-linux-readelf -a ./busybox | grep "Shared"

 0x00000001 (NEEDED)                     Shared library: [libcrypt.so.1]

 0x00000001 (NEEDED)                     Shared library: [libm.so.6]

 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

 

里面没有列出加载器,构造根文件系统时,它也要复制进去。

 

17.4  构建根文件系统

上面两节在介绍了如何安装Busbybox、C库,建立了bin/、sbin/、usr/bin/、usr/sbin/、lib/等目录──最小根文件系统的大部分目录、文件已经建好。本节介绍剩下的部分,假设单板的根文件系统在主机上的目录为/work/nfs_root/fs_mini。

17.4.1  构建etc目录

init进程根据/etc/inittab文件来创建其他子进程,比如调用脚本文件配置IP地址、挂接其他文件系统,最后启动shell等。

etc目录下的内容取决于要运行的程序,本节只需要创建3个文件:etc/inittab、etc/init.d/rcS、etc/fstab。

1.  创建etc/inittab文件

仿照Busybox的examples/inittab文件,在/work/nfs_root/fs_mini/etc目录下创建一个inittab文件,内容如下(其中各行的意义在第17.2.2节的最后有说明):

# /etc/inittab

::sysinit:/etc/init.d/rcS

ttySAC0::askfirst:-/bin/sh

::ctrlaltdel:/sbin/reboot

::shutdown:/bin/umount -a r

 

2.  创建etc/init.d/rcS文件

这是一个脚本文件,可以在里面添加想自动执行命令。以下命令完成配置IP地址、挂接/etc/fstab指定的文件系统:

#!/bin/sh

ifconfig eth0 192.168.1.17

mount -a

第一行表示这是一个脚本文件,运行时使用/bin/sh解析。

第二行用来配置IP地址。

第三行挂接/etc/fstab文件指定的所有文件系统。

最后,还要改变它的属性,使它能够执行:

chmod +x etc/init.d/rcS

 

3.  创建etc/fstab文件

内容如下,表示执行“mount -a”命令后将挂接proc、tmpfs文件系统:

# device     mount-point    type   options        dump  fsck order

proc           /proc        proc   defaults        0     0

tmpfs          /tmp         tmpfs  defaults        0     0

 

/etc/fstab文件被用来定义文件系统的“静态信息”,这些信息被用来控制mount命令的行为。文件中各字段意义如下:

① device:要挂接的设备

比如/dev/hda2、/dev/mtdblock1等设备文件;也可以是其他格式,比如对于proc文件系统这个字段没有意义,可以是任意值;对于NFS文件系统,这个字段为<host>:<dir>。

② mount-point:挂接点

③ type:文件系统类型

比如proc、jffs2、yaffs、ext2、nfs等;也可以是auto,表示自动检测文件系统类型。

④ options:挂接参数,以逗号隔开。

/etc/fstab的作用不仅仅是用来控制“mount -a”的行为,即使是一般的mount命令也受它控制,这可以从表17.8的参数看到。除与文件系统类型相关的参数外,常用的有以下几种取值:

17.8                         /etc/fstab参数字段常用的取值

参数名 说明 默认值
auto
noauto

决定执行“mount -a”时是否自动挂接。

auto:挂接;noauto:不挂接

auto
user
nouser

user:允许普通用户挂接设备;

nouser:只允许root用户挂接设备

nouser
exec
noexec

exec:允许运行所挂接设备上的程序

noexec:不允许运行所挂接设备上的程序

exec
Ro 以只读方式挂接文件系统  
rw 以读写方式挂接文件系统  
sync
async

sync:修改文件时,它会同步写入设备中;

async:不会同步写入

sync
defaults rw、suid、dev、exec、auto、nouser、async等的组合  


⑤ dump和fsck order:用来决定控制dump、fsck程序的行为。
 

dump是一个用来备份文件的程序,fsck是一个用来检查磁盘的程序。要想了解更多信息,请阅读它们的man手册。

dump程序根据dump字段的值来决定这个文件系统是否需要备份,如果没有这个字段,或其值为0,则dump程序忽略这个文件系统。

fsck程序根据fsck order字段来决定磁盘的检查顺序,一般来说对于根文件系统这个字段设为1,其他文件系统设为2。如果设为0,则fsck程序忽略这个文件系统。

 

17.4.2  构建dev目录

本节使用两种方法构建dev目录。

1.  静态创建设备文件

为简单起见,本书先使用最原始的方法处理设备:在/dev目录下静态创建各种节点(即设备文件)。

从系统启动过程可知,涉及的设备有:/dev/mtdblock*(MTD块设备)、/dev/ttySAC*(串口设备)、/dev/console、/dev/null,只要建立以下设备就可以启动系统:

$ mkdir –p /work/nfs_root/fs_mini/dev

$ cd /work/nfs_root/fs_mini/dev

$ sudo mknod console c 5 1

$ sudo mknod null c 1 3

$ sudo mknod ttySAC0 c 204 64

$ sudo mknod mtdblock0 b 31 0

$ sudo mknod mtdblock1 b 31 1

$ sudo mknod mtdblock2 b 31 2

 

注意:在一般系统中,ttySAC0的主设备号为4,但是在S3C2410、S3C2440所用的Linux 2.6.22.6上,它们的串口主设备号为204。

 

其他设备文件可以当系统启动后,使用“cat /proc/devices”命令查看内核中注册了哪些设备,然后一一创建相应的设备文件。

实际上,各个Linux系统中dev目录的内容很相似,本书最终使用的dev目录就是从其他系统中复制过来的。

 

2.  使用mdev创建设备文件

mdev是udev的简化版本,它也是通过读取内核信息来创建设备文件。

mdev的用法请参考busybox-1.7.0/doc/mdev.txt文件。mdev的用途主要有两个:初始化/dev目录、动态更新。“动态更新”不仅是更新/dev目录,还支持热拔插──接入、卸下设备时,执行某些动作,它需要内核支持“hotplugging”,即热热拔插。

要使用mdev,需要内核支持sysfs文件系统,为了减少对flash的读写,还要支持tmpfs文件系统。先确保内核已经设置了CONFIG_SYSFS、CONFIG_TMPFS配置项。

使用mdev的命令如下,请参考它们的注释以了解其作用:

$ mount -t tmpfs mdev /dev             /* 使用内存文件系统,减少对flash的读写 */

$ mkdir /dev/pts                       /* devpts用来支持外部网络连接(telnet)的虚拟终端 */

$ mount -t devpts devpts /dev/pts

$ mount -t sysfs sysfs /sys                   /* mdev通过sysfs文件系统获得设备信息 */

$ echo /bin/mdev > /proc/sys/kernel/hotplug   /* 设置内核,当有设备拔插时调用/bin/mdev程序 */

$ mdev s                                     /* 在/dev目录下生成内核支持的所有设备的结点 */

 

要在内核启动时,自动运行mdev。这要修改/work/nfs_root/fs_mini中的两个文件:修改etc/fstab来自动挂载文件系统、修改etc/init.d/rcS加入要自动运行的命令。修改后的文件如下:

① etc/fstab:

# device     mount-point    type   options        dump  fsck order

proc           /proc        proc   defaults        0     0

tmpfs          /tmp         tmpfs  defaults        0     0

sysfs          /sys         sysfs  defaults        0     0

tmpfs          /dev         tmpfs  defaults        0     0

 

② etc/init.d/rcS:加入下面几行

mount -a

mkdir /dev/pts

mount -t devpts devpts /dev/pts

echo /sbin/mdev > /proc/sys/kernel/hotplug

mdev -s

 

需要注意的是,单板上通过mdev生成的/dev目录中,S3C2410、S3C2440是串口名是s3c2410_serial0、1、 2,不是ttySAC0、1、2。需要修改etc/inittab文件:

ttySAC0::askfirst:-/bin/sh

改为:

s3c2410_serial0::askfirst:-/bin/sh

 

另外,mdev是通过init进程来启动的,在使用mdev构造/dev目录之前,init进程至少要用到设备文件/dev/console、/dev/null。所以要建立这两个设备文件:

$ mkdir –p /work/nfs_root/fs_mini/dev

$ cd /work/nfs_root/fs_mini/dev

$ sudo mknod console c 5 1

$ sudo mknod null c 1 3

 

17.4.3  构建其他目录

其他目录可以是空目录,比如proc、mnt、tmp、sys、root等,如下创建:

# cd /work/nfs_root/fs_mini

# mkdir proc mnt tmp sys root

 

现在,/work/nfs_root/fs_mini目录下就是一个非常小的根文件系统。单板可以将它作为网络根文件系统直接启动。如果要烧入单板,还要将它制作为一个文件──称为映象文件,这在下节介绍。

 

17.4.4  制作/使用yaffs文件系统映象文件

按照前面的方法,在/work/nfs_root目录下构造了两个根文件系统:fs_mini、fs_mini_mdev。前者使用dev/目录中事先建立好的设备文件,后者使用mdev机制来生成dev/目录,它们的差别只在于3点:etc/inittab文件、etc/init.d/rcS文件、dev/目录。下面两节以/work/nfs_root/fs_mini为例制作根文件系统映象。

 

所谓制作文件系统映象文件,就是将一个目录下的所有内容按照一定的格式存放到一个文件中,这个文件可以直接烧写到存储设备上去。当系统启动后挂接这个设备,就可以看到与原来目录一样的内容。

制作不同类型的文件系统映象文件,要使用不同的工具。

1.  修改制作yaffs映象文件的工具

在yaffs源码中有个utils目录(假设这个目录为/work/system/Development/yaffs2/utils),里面是工具mkyaffsimage和mkyaffs2image的源代码。前者用来制作yaffs1映象文件,后者用来制作yaffs2映象文件。

目前mkyaffsimage工具只能生成老格式的yaffs1映象文件,需要修改才能支持新格式。对mkyaffsimage代码的修改都在补丁文件yaffs_util_mkyaffsimage.patch中,读者可以直接打补丁,也可以根据本小节进行修改。

yaffs1新、老格式的不同在于oob区的使用发生了变化:一是ECC较验码的位置发生了变化,二是可用空间即标记(tag)的数据结构定义发生了变化。

另外,由于配置内核时没有设置CONFIG_YAFFS_DOES_ECC,yaffs文件系统将使用MTD设备层的ECC较验方法,制作映象文件时也使用与MTD设备层相同的函数计算ECC码。

① oob区中较验码的位置变化:

oob区中使用6字节来存放ECC较验码,前3字节对应上半页,后3字节对应下半页。

参考第16章中“yaffs文件系统移植”小节,从nand_oob_16结构可知,以前的较验码在oob区中存放的位置为8、9、10、13、14和15,现在改为0、1、2、3、6和7。

 

② oob区中可用空间的数据结构定义变化:

oob区中可用的空间有8字节,它用来存放文件系统的数据,代码中这些数据被称为标记(tag)。

老格式的yaffs1中,这8字节的数据结构如下定义(在yaffs_guts.h文件中):

typedef struct {

    unsigned chunkId:20;

    unsigned serialNumber:2;

    unsigned byteCount:10;

    unsigned objectId:18;

    unsigned ecc:12;

    unsigned unusedStuff:2;

} yaffs_Tags;

 

新格式的yaffs1中,它如下定义(在yaffs_packedtags1.h文件中):

typedef struct {

    unsigned chunkId:20;

    unsigned serialNumber:2;

    unsigned byteCount:10;

    unsigned objectId:18;

    unsigned ecc:12;

    unsigned deleted:1;

    unsigned unusedStuff:1;

    unsigned shouldBeFF;      /* 新格式中,这个字节没有使用,yaffs_PackedTags1还是8字节 */

} yaffs_PackedTags1;

 

新、老结构有细微差别:老结构中有两位没有使用(unusedStuff);新结构中只有一位没有使用,另一位(deleted)被用来表示当前页是否已经删除。

 

③ oob区中ECC码的计算:

如果配置内核时设置了CONFIG_YAFFS_DOES_ECC,则yaffs文件系统将使用yaffs2/yaffs_ecc.c文件中的yaffs_ECCCalculate函数来计算ECC码;否则使用drivers/mtd/nand/nand_ecc.c文件中的nand_calculate_ecc函数。

mkyaffsimage工具原来的代码中使用yaffs_ECCCalculate函数。 由于上面配置内核时,没有选择CONFIG_YAFFS_DOES_ECC,为了使映象文件与内核保持一致,要修改mkyaffsimage源码,使用nand_calculate_ecc函数

 

对mkyaffsimage的修改就是依据这3点进行:

① 增加头文件:

修改文件mkyaffsimage.c,加上下面这行,里面定义了yaffs_PackedTags1结构:

#include "yaffs_packedtags1.h"

 

② 修改mkyaffsimage.c文件的write_chunk函数:

代码如下:

231 static int write_chunk(__u8 *data, __u32 objId, __u32 chunkId, __u32 nBytes)

232 {

233 #ifdef CONFIG_YAFFS_9BYTE_TAGS   /* 如果要生成老格式的yaffs1映象文件,定义这个宏 */

…… /* 原来的代码 */

260 #else

261     yaffs_PackedTags1 pt1;

262     yaffs_ExtendedTags  etags;

263     __u8 ecc_code[6];

264     __u8 oobbuf[16];

265     

266     /* 写页数据,512字节 */

267     error = write(outFile,data,512);

268     if(error < 0) return error;

269

270     /* 构造tag */

271     etags.chunkId       = chunkId;

272     etags.serialNumber  = 0;

273     etags.byteCount     = nBytes;

274     etags.objectId      = objId;

275     etags.chunkDeleted  = 0;

276

277     /*

278      * 重定位oob区中的可用数据(称为tag)

279      */

280     yaffs_PackTags1(&pt1, &etags);

281

282     /* 计算tag本身的ECC码 */

283     yaffs_CalcTagsECC((yaffs_Tags *)&pt1);

284

285     memset(oobbuf, 0xff, 16);

286     memcpy(oobbuf+8, &pt1, 8);

287

288     /*

289      * 使用与内核MTD层相同的方法计算一页数据(512字节)的ECC码

290      * 并把它们填入oob

291      */

292     nand_calculate_ecc(data, &ecc_code[0]);

293     nand_calculate_ecc(data+256, &ecc_code[3]);

294

295     oobbuf[0] = ecc_code[0];

296     oobbuf[1] = ecc_code[1];

297     oobbuf[2] = ecc_code[2];

298     oobbuf[3] = ecc_code[3];

299     oobbuf[6] = ecc_code[4];

300     oobbuf[7] = ecc_code[5];

301

302     nPages++;

303

304     /* 写oob数据,16字节 */

305     return write(outFile, oobbuf, 16);

306 #endif  

307 }

308 

 

值得注意的是:第275行设置新tag结构中增加的chunkDeleted成员;第292~300行将计算出来的ECC码填入新的ECC位置──它正是nand_oob_16结构的eccpos数组定义的位置。

其中第292、293行的nand_calculate_ecc函数是从内核源文件drivers/mtd/nand/nand_ecc.c修改而来:在/work/system/Development/yaffs2/utils目录下新建一个同名文件nand_ecc.c,把内核文件nand_ecc.c的nand_calculate_ecc函数、函数中用到的nand_ecc_precalc_table数组摘出来;并去除函数中的第一个形参“struct mtd_info *mtd”──在这个函数中没用到这个参数。

 

③ 添加文件,修改Makefile:

第280行的yaffs_PackTags1函数在上一层目录yaffs_packedtags1.c中定义,先将这个文件复制到当前目录:

$ cp ../yaffs_packedtags1.c ./

 

另外,and_calculate_ecc函数是在新加的nand_ecc.c中定义的,所以要修改Makefile,把yaffs_packedtags1.c和nand_ecc.c也编译进mkyaffsimage工具中:

31 MKYAFFSSOURCES = mkyaffsimage.c

改为:

31 MKYAFFSSOURCES = mkyaffsimage.c yaffs_packedtags1.c nand_ecc.c

 

现在,在/work/system/Development/yaffs2/utils目录下执行“make”命令生成mkyaffsimage工具,将它复制到/usr/local/bin目录:

$ sudo cp mkyaffsimage /usr/local/bin

$ sudo chmod +x /usr/local/bin/mkyaffsimage

 

2.  制作/烧写yaffs映象文件

使用如下命令将/work/nfs_root/fs_mini目录制作为fs_mini.yaffs文件:

# cd /work/nfs_root

# mkyaffsimage fs_mini fs_mini.yaffs

 

将fs_mini.yaffs放入tftp目录或nfs目录后,在U-Boot控制界面就可以下载、烧入NAND Flash中,操作方法请参考《15.2.6  U-Boot的常用命令》。为方便读者,将命令列出来(以下命令将yaffs.img烧入MTD2分区,即yaffs分区):

① tftp 0x30000000 fs_mini.yaffs 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/fs_mini.yaffs

② nand erase 0xA00000 0x3600000

③ nand write.yaffs 0x30000000 0xA00000 $(filesize)

 

现在可以修改命令行参数以MTD2分区作为根文件系统,比如在U-Boot控制界面如下设置:

# set bootargs noinitrd console=ttySAC0 root=/dev/mtdblock2 rootfstype=yaffs

# saveenv

 

 

17.4.5  制作/使用jffs2文件系统映象文件

1.  编译制作yaffs映象文件的工具

/work/tools/mtd-utils-05.07.23.tar.bz2是MTD设备的工具包,编译它生成mkfs.jffs2工具,用它来将一个目录制作成jffs2文件系统映象文件。

这个工具包需要zlib压缩包,先安装zlib。在/work/GUI/xwindow/X/deps下有zlib源码zlib-1.2.3.tar.gz,执行以下命令进行安装:

$ cd /work/GUI/xwindow/X/deps

$ tar xzf zlib-1.2.3.tar.gz

$ cd zlib-1.2.3

$ ./configure --shared --prefix=/usr

$ make

$ sudo make install

 

然后编译mkfs.jffs2:

$ cd /work/tools

$ tar xjf mtd-utils-05.07.23.tar.bz2

$ cd mtd-utils-05.07.23/util

$ make

$ sudo make install

 

2.  制作/烧写jffs2映象文件

使用如下命令将/work/nfs_root/fs_mini目录制作为fs_mini.jffs2文件:

$ cd /work/nfs_root

$ mkfs.jffs2 -n -s 512 -e 16KiB -d fs_mini -o fs_mini.jffs2

上面命令中,“-n”表示不要在每个擦除块上都加上清除标志,“-s 512”指明一页大小为512字节,“-e 16KiB”指明一个擦除块大小为16KB,“-d”表示根文件系统目录,“-o”表示输出文件。

 

将fs_mini.jffs2放入tftp目录或nfs目录后,在U-Boot控制界面就可以将下载、烧入NAND Flash中,操作方法请参考《15.2.6  U-Boot的常用命令》。为方便读者,将命令列出来(以下命令将jffs2.img烧入MTD1分区,即jffs2分区):

① tftp 0x30000000 fs_mini.jffs2 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/fs_mini.jffs2

② nand erase 0x200000 0x800000

③ nand write.jffs2 0x30000000 0x200000 $(filesize)

 

系统启动后,就可以使用“mount -t jffs2 /dev/mtdblock1 /mnt”挂接jffs2文件系统。

也可以修改命令行参数以MTD1分区作为根文件系统,比如在U-Boot控制界面如下设置:

# set bootargs noinitrd console=ttySAC0 root=/dev/mtdblock1 rootfstype=jffs2

# saveenv

没有更多推荐了,返回首页