1.文件系统
1.1文件存储
首先了解如下文件存储相关概念:inode、 dentry、 数据存储、文件系统。
1.1.1inode
其本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位置……也叫作文件属性管理结构,大多数的inode都存储在磁盘上。
少量常用、近期使用的inode会被缓存到内存中。
1.1.3dentry
目录项,其本质依然是结构体,重要成员变量有两个 {文件名,inode,...},而文件内容(data)保存在磁盘盘块中。
1.1.4文件系统
文件系统是,一组规则,规定对文件的存储及读取的一般方法。文件系统在磁盘格式化过程中指定。
常见的文件系统有:fat32 ntfs exfat ext2 、ext3 、ext4
1.2 ext2文件系统
我们知道,一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(例如某种mkfs命令)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局的信息。下图是一个磁盘分区格式化成ext2文件系统后的存储布局。
文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节。而上图中启动块(BootBlock)的大小是确定的,就是1KB,启动块是由PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由以下部分组成。
超级块(Super Block) 描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。
块组描述符表(GDT,Group Descriptor Table) 由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。
块位图(Block Bitmap) 一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。
为什么用df命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用du命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用mke2fs格式化时默认块大小是1024字节,可以用-b参数指定块大小,现在设块大小指定为b字节,那么一个块可以有8b个bit,这样大小的一个块位图就可以表示8b个块的占用情况,因此一个块组最多可以有8b个块,如果整个分区有s个块,那么就可以有s/(8b)个块组。格式化时可以用-g参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs工具会计算出最优的数值。
inode位图(inode Bitmap) 和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。
**inode表(inode Table) **我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls -l命令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有inode组成了inode表。inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个inode,换句话说,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费。如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候inode会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用mke2fs的-i参数手动指定每多少个字节分配一个inode。
inode表中每个inode的大小:ext2、ext3中128字节,ext4中256字节。
数据块(Data Block) 根据不同的文件类型有以下几种情况:
- 对于常规文件,文件的数据存储在数据块中。
- 对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l命令看到的其它信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。
- 对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
- 设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
2.文件操作
2.1stat函数
获取文件属性(从inode结构体中获取)
int stat(const char *path, struct stat *buf);
成返回0;失败返回-1 设置errno为恰当值。
参数1:文件名
参数2:inode结构体指针 (传出参数)
文件属性将通过传出参数返回给调用者。
练习:使用stat函数查看文件属性
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
struct stat buf;
int ret = stat(argv[1], &buf);
if(ret == -1){
perror("stat error");
exit(1);
}
//用man 2 stat查看stat结构体
printf("st_size = %ld\n", buf.st_size);
//当对软连接查看文件属性时,stat会穿透这个连接别名直接找到原文件本身
//所以查看到连接文件时,是regular而不是link
//避免这种情况,只能使用lstat函数
if(S_ISREG(buf.st_mode)){
printf("it is a regular\n");
}else if(S_ISDIR(buf.st_mode)){
printf("it is a dictionary\n");
}else if(S_ISLNK(buf.st_mode)){
printf("it is a sym link\n");
}else if(S_ISFIFO(buf.st_mode)){
printf("it is a pipe\n");
}
printf("st_ino = %ld\n", buf.st_ino);
printf("st_nlink = %d\n", buf.st_nlink);
printf("st_uid = %d\n", buf.st_uid);
printf("st_gid = %d\n", buf.st_gid);
return 0;
}
2.2lstat函数
int lstat(const char *path, struct stat *buf);
//成返回0;失败返回-1 设置errno为恰当值。
文件类型判断方法:st_mode 取高4位。 但应使用宏函数:
S_ISREG(m) is it a regular file?
S_ISDIR(m) directory?
S_ISCHR(m) character device?
S_ISBLK(m) block device?
S_ISFIFO(m) FIFO (named pipe)?
S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.)
S_ISSOCK(m) socket? (Not in POSIX.1-1996.)
穿透符号链接:stat:会;lstat:不会
3.特殊权限位
包含三个二进制位。依次是:设置组ID位setGID;设置用户ID位setID;黏住位sticky
3.1黏住位
早起计算机内存紧,只有精要的常用的程序可以常驻物理内存,剩下的要暂存磁盘中。当内存不够用的时候会将该部分程序存回磁盘,腾出内存空间。若文件设置了黏住位,那么即使在内存比较吃紧的情况下,也不会将该文件回存到磁盘上。由于现阶段操作系统的虚拟内存管理分页算法完善。该功能已经被废弃。
但我们仍然可以对目录设置黏住位。被设置了该位的目录,其内部文件只有:
① 超级管理员
② 该目录所有者
③ 该文件的所有者
以上三种用户有权限做删除操作。其他用户可以写、读但不能随意删除。
3.2setUID位
进程有两个ID:
EID(有效用户ID),表示进程履行哪个用户的权限。
UID(实际用户ID),表示进程实际属于哪个用户。
例如:当进程执行一个root用户的文件,若该文件的setID位被设置为1, 那么执行该文件时,进程的UID不变。EID变为root,表示进程开始履行root用户权限。
setGID位于setID相类似。
3.3access函数
测试指定文件是否存在/拥有某种权限。
int access(const char *pathname, int mode);
成功/具备该权限:0;
失败/不具备 -1 设置errno为相应值。
参数2:R_OK、W_OK、X_OK//读权限,写权限,操作权限
通常使用access函数来测试某个文件是否存在。F_OK
-------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int ret;
if ((ret = access("abc", W_OK)) < 0) {
printf("error ret = %d\n", ret);
perror("abc");
exit(1);
}
printf("ret = %d\n", ret);
printf("abc is exist\n");
return 0;
}
3.4chmod函数
修改文件的访问权限
int chmod(const char *path, mode_t mode);
成功:0;失败:-1设置errno为相应值
mode:数字权限
int fchmod(int fd, mode_t mode);
3.5truncate函数
截断文件长度成指定长度。常用来拓展文件大小,代替lseek。
int truncate(const char *path, off_t length);//文件必须已经存在 成功:0;失败:-1设置errno为相应值
int ftruncate(int fd, off_t length);//可以用文件描述符来修改
----------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main(void)
{
int fd;
fd = open("./file.t", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(1);
}
#if 0
lseek(fd, 99, SEEK_SET);
write(fd, "s", 1);
close(fd);
#endif
int ret = truncate("file.t", 200);
if (ret < 0) {
perror("open error");
exit(1);
}
close(fd);
return 0;
}
3.6link函数
思考,为什么目录项要游离于inode之外,画蛇添足般的将文件名单独存储呢??这样的存储方式有什么样的好处呢?
其目的是为了实现文件共享。Linux允许多个目录项共享一个inode,即共享盘块(data)。不同文件名,在人类眼中将它理解成两个文件,但是在内核眼里是同一个文件。
link函数,可以为已经存在的文件创建目录项(硬链接)。
int link(const char *oldpath, const char *newpath);
成功:0;失败:-1设置errno为相应值
注意:由于两个参数可以使用“相对/绝对路径+文件名”的方式来指定,所以易出错。
如:link("../abc/a.c", "../ioc/b.c")若a.c,b.c都对, 但abc,ioc目录不存在也会失败。
mv命令是修改了目录项,而并不修改文件本身。
3.7unlink函数
删除一个文件的目录项;
int unlink(const char *pathname);
成功:0;失败:-1设置errno为相应值
练习:编程实现mv命令的改名操作
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
if(link(argv[1], argv[2]) == -1){
perror("link error");
exit(1);
}
if(unlink(argv[1]) == -1){
perror("unlink error");
exit(1);
}
return 0;
}
注意Linux下删除文件的机制:不断将st_nlink -1,直至减到0为止。无目录项对应的文件,将会被操作系统择机释放。(具体时间由系统内部调度算法决定)
因此,我们删除文件,从某种意义上说,只是让文件具备了被释放的条件。
unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放。要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉。
/*
*unlink函数是删除一个dentry
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
int fd;
char *p = "test of unlink\n";
char *p2 = "after write something.\n";
fd = open("temp.txt", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open temp error");
exit(1);
}
int ret = unlink("temp.txt"); //具备了被释放的条件
if(ret < 0){
perror("unlink error");
exit(1);
}
ret = write(fd, p, strlen(p));
if (ret == -1) {
perror("-----write error");
}
p[0] = 'H';//这个出错!
printf("hi! I'm printf\n");
ret = write(fd, p2, strlen(p2));
if (ret == -1) {
perror("-----write error");
}
printf("Enter anykey continue\n");
getchar();
close(fd);
return 0;
}
3.8隐式回收
当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源。
3.9symlink函数
创建一个符号链接
int symlink(const char *oldpath, const char *newpath);
成功:0;失败:-1设置errno为相应值
3.10readlink函数
读取符号链接文件本身内容,得到链接所指向的文件名。
ssize_t readlink(const char *path, char *buf, size_t bufsiz);
成功:返回实际读到的字节数;失败:-1设置errno为相应值。
3.11rename函数
重命名一个文件。
int rename(const char *oldpath, const char *newpath);
成功:0;失败:-1设置errno为相应值
4.目录操作
工作目录:“./”代表当前目录,指的是进程当前的工作目录,默认是进程所执行的程序所在的目录位置。
4.1getcwd函数
获取进程当前工作目录 (卷3,标库函数)
char *getcwd(char *buf, size_t size);//sizeof(buf)
成功:buf中保存当前进程工作目录位置。失败返回NULL。
4.2chdir函数
改变当前进程的工作目录
int chdir(const char *path);
成功:0;失败:-1设置errno为相应值
练习:获取及修改当前进程的工作目录,并打印至屏幕。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
char buf[64];
char *p;
system("ls -l");
int ret = chdir(argv[1]); //改变当前进程工作目录
if(ret < 0){
perror("chdir error");
exit(1);
}
system("ls -l");
chdir(".."); //改变当前进程当前工作目录
system("ls -l");
return 0;
}
4.3文件、目录权限
注意:目录文件也是“文件”。其文件内容是该目录下所有子文件的目录项dentry。 可以尝试用vim打开一个目录
权限 | r | w | x |
---|---|---|---|
文件 | 文件的内容可以被查看:cat、more、less... | 内容可以被修改vi、> | 可以运行产生一个进程 ./文件名 |
目录 | 目录可以被浏览ls、tree | 创建、删除、修改文件mv touch mkdir | 可以被打开、进入cd |
目录设置黏住位:若有w权限,创建不变,删除、修改只能由root、目录所有者、文件所有者操作。
4.4opendir/closedir函数
根据传入的目录名打开/关闭一个目录 (库函数) DIR * 类似于 FILE *
DIR *opendir(const char *name);
成功返回指向该目录结构体指针,失败返回NULL
//关闭打开的目录
int closedir(DIR *dirp);
成功:0;失败:-1设置errno为相应值
参数支持相对路径、绝对路径两种方式:例如:打开当前目录:① getcwd() , opendir() ② opendir(".");
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int count(char *root)
{
DIR *dp;
struct dirent *item;
int n = 0;
dp = opendir(root);
if (dp == NULL) {
perror("opendir error");
exit(1);
//count是次级函数,出错应返回一个自定义个错误号给调用者处理
//这里直接调用exit退出。
}
while ((item = readdir(dp))) { //遍历每一个目录项,NULL表示读完
struct stat statbuf;
printf("=======想想程序为什么出错?=======\n");
/*取文件的属性, lstat防止穿透*/
if(lstat(item->d_name, &statbuf) == -1){
perror("lstat error");
exit(1);
}
if (S_ISREG(statbuf.st_mode)) {
n++;
} else if (S_ISDIR(statbuf.st_mode))
n += count(item->d_name);
}
closedir(dp);
return n;
}
int main(void)
{
int total;
total = count("./");
//printf("There are %d files in %s\n", total, argv[1]);
printf("There are %d files in ./\n", total);
return 0;
}
4.5readdir函数
读取目录 (库函数)
struct dirent *readdir(DIR *dirp);
成功返回目录项结构体指针;失败返回NULL设置errno为相应值
需注意返回值,读取数据结束时也返回NULL值,所以应借助errno进一步加以区分。
struct 结构体:
struct dirent {
ino_t d_ino; inode编号
off_t d_off;
unsigned short d_reclen; 文件名有效长度
unsigned char d_type; 类型(vim打开看到的类似@*/等)
char d_name[256]; 文件名
};
其成员变量重点记忆两个:d_ino、d_name。实际应用中只使用到d_name。
实现ls不打印隐藏文件。每5个文件换一个行显示。
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
DIR *dp;
struct dirent *sdp;
int count = 0;
dp = opendir(".");
if(dp == NULL){
perror("opendir error");
exit(1);
}
while((sdp = readdir(dp)) != NULL){
if(sdp->d_name[0] != '.')
printf("%-10s%c", sdp->d_name, ++count % 5? '\t' : '\n');
}
closedir(dp);
return 0;
}
4.6rewinddir函数
回卷目录读写位置至起始。
void rewinddir(DIR *dirp); 返回值:无。
4.7telldir/seekdir函数
获取目录读写位置
long telldir(DIR *dirp);
成功:与dirp相关的目录当前读写位置。失败-1,设置errno
修改目录读写位置
void seekdir(DIR *dirp, long loc);
返回值:无
参数loc一般由telldir函数的返回值来决定。
递归遍历目录
查询指定目录,递归列出目录中文件,同时显示文件大小。
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PATH_LEN 256
//dir=/home/itcast/1110_linux fcn=isfile
void fetchdir(const char *dir, void (*fcn)(char *))//该函数被调用,既已被判定为目录
{
char name[PATH_LEN];
struct dirent *sdp;
DIR *dp;
if ((dp = opendir(dir)) == NULL) { //打开目录失败,如:没有x权限
//perror("fetchdir can't open");
fprintf(stderr, "fetchdir: can't open %s\n", dir);
return;
}
//
while ((sdp = readdir(dp)) != NULL) {
if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) { //防止出现无限递归
continue;
}
if (strlen(dir)+strlen(sdp->d_name)+2 > sizeof(name)) {
fprintf(stderr, "fetchdir: name %s %s too long\n", dir, sdp->d_name);
} else {
sprintf(name, "%s/%s", dir, sdp->d_name);
(*fcn)(name); //这是一个什么??
}
}
closedir(dp);
}
void isfile(char *name) //处理目录/文件
{
struct stat sbuf;
if (stat(name, &sbuf) == -1) { //文件名无效
fprintf(stderr, "isfile: can't access %s\n", name);
exit(1);
}
if ((sbuf.st_mode & S_IFMT) == S_IFDIR) { //判定是否为目录
fetchdir(name, isfile); //回调函数,谁是回调函数呢?
}
printf("%8ld %s\n", sbuf.st_size, name); //不是目录,则是普通文件,直接打印文件名
}
//./ls_R ~/1110_linux
int main(int argc, char *argv[])
{
if (argc == 1)
isfile(".");
else
while (--argc > 0) //可一次查询多个目录
isfile(*++argv); //循环调用该函数处理各个命令行传入的目录
return 0;
}