Unix C:文件系统

Linux文件目录构成

树状目录结构

  • /bin:bin 是 Binaries (二进制文件) 的缩写,这个目录存放着最经常使用的命令。

  • /boot:这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。

  • /dev:dev 是 Device(设备) 的缩写,该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的(一切皆文件)。

  • /etc:etc 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件和子目录。

  • /home:用户的主目录(家目录),在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve。

  • /lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。

  • /lost+found:这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

  • /media:linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。

  • /mnt:系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。

  • /opt:opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

  • /proc:proc 是 Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。

  • /root:该目录为系统管理员,也称作超级权限者的用户主目录。

  • /sbin:s 就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序。

  • /tmp:tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。

  • /usr:usr 是 unix shared resources (共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的C:/Windows/ 目录。

    • /usr/lib理解为C:/Windows/System32
    • /usr/local:用户级的程序目录,可以理解为C:/Progrem Files/。用户自己编译的软件默认会安装到这个目录下。
    • /opt:用户级的程序目录,可以理解为D:/Software,opt有可选的意思,这里可以用于放置第三方大型软件(或游戏),当你不需要时,直接rm -rf掉即可。
  • /var:var 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

  • /run:是一个临时文件系统,存储系统启动以来的信息。当系统重启时,这个目录下的文件应该被删掉或清除。如果你的系统上有 /var/run 目录,应该让它指向 run。

切换用户

su 用户名
su - root

~代表当前登录用户的用户目录(家目录)。

如果当前的用户是root,则~代表/root

[lighthouse@HongyiZeng ~]$ su root
Password: 
[root@HongyiZeng lighthouse]# cd ~
[root@HongyiZeng ~]# pwd
/root

如果当前的用户是其他用户,则~代表/home/用户名

[lighthouse@HongyiZeng ~]$ pwd
/home/lighthouse

/etc/passwd

/etc/passwd为用户信息文件存放路径。

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
chrony:x:997:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
syslog:x:996:994::/home/syslog:/bin/false
lighthouse:x:1000:1000::/home/lighthouse:/bin/bash

每一行分为7个字段,以冒号:进行分割,含义如下:

字段 含义
用户名 用户登录系统时使用的用户名
密码 密码位,通常将passwd文件中的口令字段使用一个x来代替,将/etc /shadow作为真正的口令文件
UID 用户标识号
GID 缺省组标识号
注释性描述 例如存放用户全名等信息
宿主目录 用户登录系统后的缺省目录
命令解释器 用户使用的shell,默认为bash

/etc/group

/ect/group 文件是用户组配置文件,即用户组的所有信息都存放在此文件中。只列出部分:

root:x:0:
lighthouse:x:1000:lighthouse

每一行分为4个字段,以冒号:进行分割,含义如下:

字段 含义
组名 用户组名称
组密码 和 /etc/passwd 文件一样,这里的 “x” 仅仅是密码标识
GID 组标识号
组中的用户 此字段列出每个群组包含的所有用户

/usr/include

linux系统编程往往需要引用c头文件,linux下,头文件一般存储到/usr/include

文件和目录

stat

stat,系统调用,用于获取文件的属性。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
  • stat:第一个形参:文件路径; 第二个形参:一个指向结构体stat的指针,因此需要传入结构体的地址;
  • fstat:第一个形参是文件描述符;
  • lstat:lstat函数的形参跟stat函数的形参一样。其功能也跟stat函数功能一样,仅有一点不同:stat函数是穿透(追踪)函数,即对软链接文件进行操作时,操作的是链接到的那一个文件,不是软链接文件本身;而lstat函数是不穿透(不追踪)函数,对软链接文件进行操作时,操作的是软链接文件本身。注:软链接严格来说应该叫符号链接
  • 返回值:成功返回0,失败返回-1,并且将详细错误信息赋值给errno全局变量。

struct stat类型的说明:

struct stat {
    dev_t st_dev;     /* 文件的设备编号 */
    ino_t st_ino;     /* 索引结点编号 */
    mode_t st_mode;    /* 文件类型和权限*/
    nlink_t st_nlink;   /*硬链接数 */
    uid_t st_uid;     /*用户ID*/
    gid_t st_gid;     /* 组ID*/
    dev_t st_rdev;    /* 设备类型(若此文件为设备文件,则为设备编号*/
    off_t st_size;    /* 文件大小,以字节为单位*/
    blksize_t st_blksize; /*文件系统的I/O块大小*/
    blkcnt_t st_blocks;  /* 块数 */
    time_t st_atime;   /* 访问时间 */
    time_t st_mtime;   /* 修改时间 */
    time_t st_ctime;   /* 更改时间 */
}; 

代码示例

打印出文件的大小。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 注意返回值是off_t类型
static off_t flen(const char *fname) {
    struct stat statres; // 声明一个stat类型的结构体statres
    if(stat(fname, &statres) < 0) {
        perror("stat()");
        exit(1);
    }
	// 返回st_size成员
    return statres.st_size;
}

int main(int argc, char **argv) {
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    printf("total size: %lld\n", flen(argv[1]));

    exit(0);
}

off_t类型用于指示文件的偏移量,通常就是long类型,其默认为一个32位的整数,在gcc编译中会被编译为long int类型,在64位的Linux系统中则会被编译为long long int,这是一个64位的整数,其定义在unistd.h头文件中可以查看。

# ifndef __off_t_defined
#  ifndef __USE_FILE_OFFSET64
typedef __off_t off_t;
#  else
typedef __off64_t off_t;
#  endif
#  define __off_t_defined
# endif
# if defined __USE_LARGEFILE64 && !defined __off64_t_defined
typedef __off64_t off64_t;
#  define __off64_t_defined
# endif

空洞文件

在描述文件属性的结构体stat中,有以下三个描述文件大小的成员:

C
struct stat {
    off_t st_size;    /* 文件大小,以字节为单位*/
    blksize_t st_blksize; /*文件系统的I/O块大小*/
    blkcnt_t st_blocks;  /* 块数 */
}; 

其中,块大小一般为4096字节,即4KB(一个块为连续8个扇区,每个扇区为512B);块数为该文件的占用的块数;

注意:st_size ≠ st_blksize * st_blocks;或者说,st_size是文件的逻辑大小,而st_blksize * st_blocks是文件的物理大小

代码示例

创建一个5GB大小的文件:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **argv) {
    int fd;
    if(argc < 2) {
        fprintf(stderr, "Usage...");
        exit(1);
    }
    if((fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
        perror("open");
        exit(1);
    }
	// 先让指针从文件开头向后移动5G个字节
    lseek(fd, 5LL * 1024LL * 1024LL * 1024LL - 1LL, SEEK_SET);
	// 在最后写入一个空字符
    write(fd, "", 1);
    close(fd);
    exit(0);
}

执行结果:

[root@HongyiZeng fs]# make big
cc     big.c   -o big
[root@HongyiZeng fs]# ./big /tmp/bigfile
[root@HongyiZeng fs]# stat /tmp/bigfile 
  File: ‘/tmp/bigfile’
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:19:40.734216438 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 16:19:40.734216438 +0800
 Birth: -

可以看出,大小为5368709120B,但是占用的块数却为8,即实际占用的物理大小为4KB * 8 = 32KB

如果将该文件进行拷贝,再查看拷贝文件的属性:

PLAINTEXT
[root@HongyiZeng fs]# cp /tmp/bigfile /tmp/bigfile.bak
[root@HongyiZeng fs]# stat /tmp/bigfile.bak 
  File: ‘/tmp/bigfile.bak’
  Size: 5368709120      Blocks: 0           IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26200       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:26:39.166784814 +0800
Change: 2022-12-05 16:26:39.166784814 +0800
 Birth: -

使用 lseek 可以修改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),通过 lseek 系统调用可以将该文件的读写偏移量移动到偏移文件头部 6000 个字节处。

接下来使用 write 函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。

空洞文件的作用

空洞文件的好处是:空洞文件对多线程共同操作文件是很有用的。

因为我们在创建一个很大文件的时候,我们就把一个文件分成很多的段,然后采用多线程的方式,让每个线程负责写入其中的某一段的数据。这样的话比我们用单个线程写入是快很多的。

例如:

  • 在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;
  • 在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。

st_mode

st_mode是一个16位的位图,用于表示文件类型,文件访问权限以及特殊权限位。

它的类型为mode_t,其实就是普通的unsigned int,但是只是用了低16位。

st_mode的具体表示方法如下:

st_mode的位 含义
15-12位 文件类型
11-9位 设置用户ID位,设置组ID位,文件粘滞位
8-6位 关于其他用户的权限
5-3位 关于组的访问权限
2-0位 关于用户的访问权限

假设st_mode表示为八进制的100664

则有:

  • 1000: 这是一个常规文件
  • 000: 执行时设置信息为空,黏着位为 0
  • 110-110-100: 用户权限为 RW-,组员权限为RW-,其他人权限为R--

手动分析st_mode字段是很不方便的。实际写程序,可使用 st_mode & 掩码来得到st_mode中特定的部分。比如:

st_mode & 0170000 : 得到文件类型
st_mode & 0007000 : 得到执行文件时设置信息
st_mode & 0000777 : 得到权限位
st_mode & 00100: 判断所有者是否可执行
(以上数字均为八进制)

在头文件中,st_mode使用8进制表示文件类型,具体位的情况在头文件定义如下:

//bit15 ~ bit12 , 文件类型属性区域
#define S_IFMT   0170000    /* 掩码 file type mask */  
#define S_IFSOCK 0140000    /* 本地套接字 socket dgram */  
#define S_IFLNK  0120000    /* 符号链接 symbolic link */  
#define S_IFREG  0100000    /* 普通文件 regular file */  
#define S_IFBLK  0060000    /* 块设备 block device */  
#define S_IFDIR  0040000    /* 目录 directory */  
#define S_IFCHR  0020000    /* 字符设备 character device */  
#define S_IFIFO  0010000    /* 有名管道(先进先出) FIFO */

含义 数值 十进制 二进制
S_IFMT 掩码 0170000 15 1111 000 000 000 000
S_IFSOCK 本地套接字 0140000 12 1110 000 000 000 000
S_IFLNK 符号链接 0120000 10 1010 000 000 000 000
S_IFREG 普通文件 0100000 8 1000 000 000 000 000
S_IFBLK 块设备 0060000 6 0110 000 000 000 000
S_IFDIR 目录 0040000 4 0100 000 000 000 000
S_IFCHR 字符设备 0020000 2 0010 000 000 000 000
S_IFIFO 有名管道 0010000 1 0001 000 000 000 000
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)  //提供了一些宏函数来帮助用户执行&操作,是则返回1
#define S_ISLNK(m)  (((m) & S_IFMT) == S_IFLNK)  
#define S_ISREG(m)  (((m) & S_IFMT) == S_IFREG)
#define S_ISBLK(m)  (((m) & S_IFMT) == S_IFBLK)
#define S_ISDIR(m)  (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)  (((m) & S_IFMT) == S_IFCHR)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)

//bit11 ~ bit9,权限的特殊属性区域
#define  S_ISUID      0004000     文件的(set user-id on execution)位
#define  S_ISGID      0002000     文件的(set group-id on execution)位
#define  S_ISVTX      0001000     文件的sticky位

//bit8 ~ bit0,权限属性区域
//文件所有者(owner)
#define S_IRWXU 00700	/* mask for file owner permissions */
#define S_IRUSR 00400	/* owner has read permission */
#define S_IWUSR 00200	/* owner has write permission */
#define S_IXUSR 00100	/* owner has execute permission */
 //组用户(group)
#define S_IRWXG 00070	/* mask for group permissions */
#define S_IRGRP 00040	/* group has read permission */
#define S_IWGRP 00020	/* group has write permission */
#define S_IXGRP 00010	/* group has execute permission */
 //其他用户(other)
#define S_IRWXO 00007	/* mask for permissions for others (not in group) */
#define S_IROTH 00004	/* others have read permission */
#define S_IWOTH 00002	/* others have write permission */
#define S_IXOTH 00001	/* others have execute permission */
④ 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

static int ftype(const char *fname) {
    struct stat statres;

    if(stat(fname, &statres) < 0) {
        perror("stat()");
        exit(1);
    }

    if(S_ISREG(statres.st_mode))
        return '-';
    else if(S_ISDIR(statres.st_mode))
        return 'd';
    else if(S_ISSOCK(statres.st_mode))
        return 's';
    else
        return '?';
}

static int fper(const char *fname) {
    struct stat statres;

    if(stat(fname, &statres)) {
        perror("fper()");
        exit(1);
    }

    return statres.st_mode & S_IRWXU;
}


int main(int argc, char **argv) {
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    printf("File type: %c\n", ftype(argv[1]));
	printf("Permission of owner: %o\n", fper(argv[1]));
    exit(0);
}

执行结果:

[root@HongyiZeng fs]# ./ftype ftype.c 
File type: -
Permission of owner: 600
[root@HongyiZeng fs]# ./ftype ftype
File type: -
Permission of owner: 700

文件权限

umask

umask函数原型:

#include <sys/stat.h>
mode_t umask(mode_t mask);

在进程创建一个新的文件或目录时,如调用open函数创建一个新文件,新文件的实际存取权限是mode与umask按照 mode & ~umask运算以后的结果。umask函数用来修改进程的umask,作用是防止出现权限过松的文件。

chmod

补充:chmod命令

语法:

chmod [对谁操作(ugoa)] [操作符 (+-=)] [赋予的权限(rwxs或数字)] 文件名1 文件名2...

  • 在Unix和Linux系统中,可以通过chmod命令设置用户ID位、组ID位和粘住位:
    • 设置用户ID位:
      • chmod u+s filename
    • 设置组ID位:
      • chmod g+s filename
    • 设置粘滞位:
      • chmod +t filename

例如:

shared$ ll test
-rwx------ 1 tarena tarena 0 12月 16 15:11 test*
shared$ chmod +s test
shared$ ll test
-rws--S--- 1 tarena tarena 0 12月 16 15:11 test*
shared$ chmod +t test
shared$ ll test
-rws--S--T 1 tarena tarena 0 12月 16 15:11 test*
#o有可执行权限就显示-rw-rw-r-t,没有可执行权限就显示-rw-rw-r-T

这将给文件设置粘住位,使得只有文件的拥有者或超级用户可以对该文件进行写操作,不会影响文件的读权限。如果其他用户尝试修改该文件,系统将拒绝该请求并返回一个权限错误。


chmod函数原型:

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

chmod是对指定的文件进行操作,而fchmod则是对已经打开的文件进行操作。所以它们的第一个参数不一样。

返回值:如果改变成功返回0,否则返回-1

代码示例

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
    int fd = open("a", O_RDWR);
    if (-1 == fd)
    {
       perror("open fail");
       exit(1);
    }
 	// 将fd的权限更改为0777
    if(-1 == fchmod(fd, 0777))
    {
        perror("fchmod fail");
        exit(1);
    }
 	
    // 将b文件的权限更改为0777
    if (-1 == chmod("b", 0777))
    {
       perror("fchmod fail");
       exit(1);
    }
    
    close(fd);
    return 0;
}
用户ID位
  • 系统中每个进程其实都有两个用户ID,一个叫实际用户ID,一个叫有效用户ID
  • 进程的实际用户ID继承子父进程的实际用户ID,当一个用户通过合法用户名和口令登陆系统后,系统就会为他启动一个shell进程,shell进程的实际用户ID就是该登陆用户的用户ID。该用户在shell下启动的任何进程都是shell进程的子进程,自然也就继承了shell进程的实际用户ID
  • 一个进程的用户身份决定了它可以访问哪些资源,比如读写或执行某个文件。
    • 但真正被用于权限验证的并不是进程的实际用户ID,而是有效用户ID。一般情况下进程的有效用户ID就取自实际用户ID,可以认为二者是等价的
  • 如果启动进程的可执行文件设置了用户ID位,即B11位为1,则该进程的有效用户ID会取自可执行文件的拥有者用户ID
  • 系统管理员用这种方法提升普通用户权限,让他们有能力去完成一些只有root用户才能完成的任务。
    • 例如,将一个root用户创建的可执行文件设置用户ID位,这样其他用户执行这个文件时启动的进程有效用户ID都是root,该进程拥有root权限
  • 非可执行文件和目录设置用户ID位是没有意义的
组ID位
  • 组ID位同理,可执行文件设置了组ID位为1之后,启动的进程的有效组ID是文件的所属组ID
  • 设置ID位的不可执行文件:某些系统用这种方式表示强制锁
  • 设置ID位的目录文件:在该目录下创建的文件或目录的拥有者取该目录的所属组,而非创建者所属组
粘滞位
  • 可执行文件

    • 设置粘滞位后能够指示操作系统在程序退出后,保留程序的代码段到swap空间。当程序再次执行时,内核只需将程序从swap搬到内存即可,这能够加速程序的执行。因此,频繁使用的程序比如编辑器能够更快的打开。
  • 不可执行文件

    • 设置粘滞位:无意义
  • 目录

    • 设置粘住位:只有对该目录具有写权限的用户在满足下列之一的情况下,才能删除或更名该目录下的文件或目录:

      • 拥有此文件

      • 拥有此目录

      • 是超级用户

目录/tmp是设置粘住位的典型候选者:任何用户都可在这个目录中创建文件。任一用户(用户、组和其他)对这个目录的权限通常都是读、写和执行(rwx)。但是用户不应能删除或更名属于其他人的文件,为此在这个目录的文件模式中都设置了粘住位。

即:假如/tmp下的文件A被用户U1所有,文件A的权限为777,那么所有用户都可以对该文件进行修改、移动、重命名等操作,但无法删除该文件。通常的用途在于用户团队协作的目录,用户可以相互修改文件,却只有用户所有者才能删除

文件系统物理结构

一个磁盘驱动器被划分成一到多个分区,其中每个分区上都建有独立的文件系统,每个文件系统包括:

  • 引导块:计算机加电启动时,ROM BIOS从这里读取可执行代码和数据,以完成操作系统自举
  • 超级块:记录文件系统的整体信息,如文件系统的格式和大小,i结点和数据块的总量、使用量和剩余量等
  • 若干柱面组,其中每个柱面组包括:
    • 级块副本:同上
    • 柱面组信息:柱面组的整体描述
    • i节点(inode)映射表:i结点号与i节点磁盘位置的对应表
    • 块位图:位图中的每个二进制位对应一个数据块,用1和0表示该块处于占用或空闲状态
    • i节点表:包含若干i节点,记录文件的元数据和数据块索引表
    • 数据块集:包含若干数据块,存储文件的内容数据

文件系统简介

文件系统:文件或数据的存储和管理。目前,正在使用的UNIX文件系统有多种实现。

  • 传统的基于BSD的UNIX文件系统(称为UFS)。UFS是以Berkeley快速文件系统为基础的。本节讨论该文件系统。
  • 读、写DOS格式软盘的文件系统(称为PCFS
  • 读CD的文件系统(称为HSFS

我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统

  • i节点即为inode结构体的数组
  • 数据块一般被分成了大小为4KB的块(block)
  • i节点图(i节点位图):用来判断inode的空闲与占用情况
  • 块位图:用来判断数据块的占用与空闲情况

对于普通文件,如下图:

  • i节点结构体中通常包含15个指针来指向数据块,最后三个指针为一二三级指针,用于扩充文件的大小;图中的i节点指向了三个数据块。
  • 在图中有两个目录项(两个不同文件名的文件,但是inode编号相同)指向同一个i节点(此时称为硬链接,即目录项就是硬链接的同义词)。每个i节点中都有一个硬链接计数st_nlink,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。
  • i节点包含了文件有关的所有信息∶文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat 结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中∶文件名和i节点编号

对于目录,目录也是一种文件,它的属性也需要inode结构体存储,它的物理存储也需要通过inode中的指针来指向的数据块(此时的数据块就是目录块)来存储;

目录块存储的内容非常的简单,由目录项组成,每条目录项有包含的文件名以及该文件名对应的inode编号

如下图所示:

  • 编号为2549的i节点(testdir),其类型字段st_mode表示它是一个目录(因此它指向一个特殊的数据块——目录块),链接计数为2。任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录(testdir)的目录项以及在该目录中的.项。
  • 编号为1267的i节点,其类型字段st_mode表示它是一个目录,链接计数大于或等于3。它大于或等于3的原因是,至少有3个目录项指向它∶一个是命名它的目录项,第二个是在该目录中的.项,第三个是在其子目录 testdir 中的..项。注意,在父目录中的每一个子目录都使该父目录的链接计数增加1。

inode

① 简介

文件数据是储存在硬盘上的,硬盘的最小存储单位叫做扇区。每个扇区存储512字节,而连续的8个扇区组成了一个(block),大小为4kB文件数据都存储在块中,为了能够方便找到存储数据的位置我们还必须找到一个地方存储文件的属性,这种存储文具属性信息的区域叫做inode(索引节点)。

② inode的信息

inode(本质上是一个结构体)包含文件的属性信息有以下内容:注意,没有文件名和inode编号

  • 文件的字节数

  • 文件拥有者的id

  • 文件所属组id

  • 文件的读写执行权限

  • 文件的时间戳,共有三个:

    • ctime指inode上一次变动的时间
    • mtime指文件内容上一次变动时间
    • atime指文件上一次打开时间。
  • 硬链接数,即有多少个文件指向这个inode

  • 文件数据块(block)的位置,即指向数据块的指针,包括一级指针,二级指针和三级指针,一般为15个指针

    inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘划分为两个区域。一个是数据区,存放文件数据,另一个是inode区(inode table),存放inode所包含的信息,inode区本质上是一个结构体数组,数组下标就是inode编号inode_num

image-20221205182107212

每个inode节点(结构体)的大小,一般是128字节或者256字节。inode节点的总数。在格式化时就给定,一般是每1kb或者每2kb就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小就会达到128MB。

inode编号是很重要的,当系统在找一个文件时,步骤如下:

  1. 通过文件名先找到文件的inode编号(数组下标)
  2. 通过inode编号找到文件inode信息(结构体)
  3. 通过inode信息中的block指针找到文件内容。
③ 相关命令

stat显示文件的状态信息。stat命令的输出信息比ls命令的输出信息要更详细。

[root@HongyiZeng sysio]# ls
ab  ab.c  dup  dup.c  mycpy  mycpy.c  test
[root@HongyiZeng sysio]# stat ab.c
  文件: ‘ab.c’
  大小: 193             块: 8          IO 块: 4096   普通文件
设备: fd01h/64769d    Inode: 797269      硬链接: 1
权限: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
最近访问: 2022-12-02 16:05:47.044135336 +0800
最近修改: 2022-12-02 16:05:45.239158807 +0800
最近改动: 2022-12-02 16:05:45.245158729 +0800
 创建时间: -

ls -i用于查看某个文件的inode编号。

[root@HongyiZeng sysio]# ls -i ab.c
797269 ab.c
④ 硬链接

一般情况下,文件名和inode号码是一一对应关系,每个inode号码对应一个文件名,但是Linux系统允许多个文件名指向同一个inode号码,这就意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是删除一个文件名,不影响另一个文件名访问,相当于源文件的副本,这种情况叫做硬链接。

inode信息中有一项叫做链接数,记录指向该inode的文件总数,这时会加1,反过来,删除一个文件名,会减一,当链接数变为0时,表明没有文件指向这个inode号码,系统就会回收这个inode号码与文件数据块区。

⑤ 符号链接

文件A与文件B的号码虽然不一样,但是文件A的内容是文件B的路径,A就是B的软链接文件。读取文件A时,系统会自动访问导向文件B的文件名,然后再根据B的inode去访问存储在块中的数据。

而这意味着,文件A依赖于文件B而存在,若删除了文件B,打开文件A就会报错。这就是软连接与硬链接最大的不同:文件A指向文件B的文件名,而不是inode号码,文件B的inode链接数不会发生变化

数据块

数据块(data block),512/1024/4096字节

  • 直接块:存储文件的实际内容数据,包含:
    • 文件块:存储普通文件的内容数据
    • 目录块:存储目录文件的内容数据
  • 间接块:存储下级文件数据块索引表

文件类型

普通文件

  • 在Unix/Linux系统中通常见到的文件,如同C/C++语言编写的源代码文件,编译器、汇编器和链接器产生的汇编文件、目标文件和可执行文件,各种系统配置文件,Shell脚本文件,包括音频、视频等数字内容的多媒体文件,乃至包括数据库在内的各种应用程序所维护、管理和使用的数据文件等,都是普通文件
  • 组成文件的线性数组里字节的数目,即文件长度或称为文件大小,其最大值受限于Linux内核中用于管理文件的C语言代码数据类型的大小,某些文件系统还可能强加自己的限制,将其限定在更小的值
  • 操作系统内核并没有对并发文件访问强加任何控制,不同的进程能够同时读写同一个文件,并发访问的结果取决于独立操作的顺序,且通常是不可预测的
  • 一个文件包括两部分,一部分是元数据,入文件的类型、权限、大小、用户、组、各种时间戳等,存储在inode中,另一部分是内容数据,存储在数据块中

目录文件

  • 系统通过inode号唯一地标识一个文件的存在,但人民更愿意使用有意义的文件名来访问文件,目录就是用来建立文件名和inode号之间的映射的
  • 目录的本质就是一个普通文件,与其他普通文件唯一的区别就是它仅仅存储文件名和inode号的映射,每一个这样的映射,用目录中的一个条目表示,谓之硬链接
  • 目录文件也有自己的inode,每个目录的inode和它的文件名之间的映射,记录在它的父目录中,依次类推,形成了一颗目录树
  • 根目录的inode在inode表中的存储位置是固定的,因此几十没有父目录,根目录也能被正确检索
  • 当系统内核打开类似"/home/unixc/text.txt"这样的路径时,会从根目录开始遍历路径中的每一个目录项来查找下一个目录的inode号,然后直到在unixc目录中找到text.txt文件并打开
  • 如果路径字符串的第一个字符不是“/”,则表示相对路径,基于相对路径的路径解析从当前工作目录开始
  • 每个目录中都有两个特殊的条目“.”和“..”分别映射该目录本身和其父目录的inode,根目录没有父目录,所以“.”和“..”一样映射到根目录本身

符号链接文件

符号链接文件(软链接)的数据库保存的是被链接文件完整路径名

相比于硬链接,符号链接的解析需要更大的系统开销,因为有效地解析符号链接至少需要解析两个文件,符号链接文件本身和它所链接的文件

文件系统会为硬链接维护链接计数,但不会为符号链接维护任何东西,因为相交于硬链接符号链接缺乏透明性

通过命令ln -s可以创建一个符号链接

特殊文件

特殊文件是以文件形式表示的内核对象,具体包括

本地套接字:套接字是网络编程的基础,本地套接字是其面向本机通信的一个变种,它需要依赖文件系统中的一种特殊文字——本地套接字文件

字符设备:设备驱动将字节按顺序写入队列,用户程序从队列中按其被写入的顺序将字节依次读出,如键盘

块设备:设备驱动将字节数组映射到可寻址的设备上,用户程序可以任意顺序访问数组中的任意字节,如硬盘

有名管道:有名管道是一种以文件描述符为信道的进程间通信(IPC)机制,即使是不相关的进程也能通过有名管道文件交换数据

通过ls -l命令可以查看文件类型

符号 含义
- 普通文件
d 目录文件
s 本地套接字
c 字符设备
b 块设备
l 符号链接
p 有名管道

文件的删除、重命名和移动

  • remove
#include <stdio.h>
int remove(const char *pathname);
  • rename:是mv命令的系统调用
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);

更改文件的时间

  • utime:可更改文件的最后读的时间和最后修改的时间
#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

更改当前工作路径

  • chdir:是cd命令的系统调用
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
  • getcwd:是pwd命令的系统调用
#include <unistd.h>
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
char *get_current_dir_name(void);

分析读取目录

glob函数原型(模型匹配函数):

#include <glob.h>
int glob(const char *pattern, int flags,
         int (*errfunc) (const char *epath, int eerrno),
         glob_t *pglob);
// 释放glob函数调用的空间
void globfree(glob_t *pglob);
  • pattern: 通配符,要分析的pattern,如/*表示匹配根文件下的所有文件(不包括隐藏文件)
  • flags:flags参数可以设置特殊要求,如无特殊要求置为0
  • errfunc:函数指针,glob函数执行出错会执行的函数,出错的路径会回填到epath中,出错的原因回填到eerrno中。如不关注错误可设置为NULL
  • pglob:解析出来的结果放在这个参数里,是一个结构体指针
  • 返回值:成功返回0,错误返回非0

其中,glob_t是一个结构体:

typedef struct {
    // pathc与pathv类似于main函数参数argc与argv
    size_t    gl_pathc;    //匹配到的数量
    char    **gl_pathv;    //匹配到的元素放在这里
    size_t    gl_offs; 
} glob_t;

程序实例

打印出/etc/*.conf的文件名。

#include <stdio.h>
#include <stdlib.h>
#include <glob.h>
// 通配符
#define PAT "//etc//*.conf"

static int errfunc(const char *epath, int eerrno) {
    puts(epath);
    fprintf(stderr, "ERROR: %d", eerrno);
    return 1;
}

int main() {
    glob_t globres;
    int err;
    err = glob(PAT, 0, errfunc, &globres);

    if(err) {
        printf("ERROR CODE = %d\n", err);
        exit(1);
    }

    int i;

    for(i = 0; i < globres.gl_pathc; i++) {
        puts(globres.gl_pathv[i]);
    }
    // 释放空间
	globfree(&globres);
    exit(0);
}

执行结果:

[root@HongyiZeng fs]# ./glob 
/etc/GeoIP.conf
/etc/asound.conf
/etc/chrony.conf
/etc/dat.conf
其余略

一组对目录操作的函数

和对文件操作的函数基于FILE结构体类似,对目录的操作基于名为DIR的结构体。

下面是常用的对目录进行操作的函数,他们的功能可以被glob替代。

posted @ 2024-12-06 17:13  -O-n-e-  阅读(98)  评论(0)    收藏  举报