介绍linux系统编程文件系统相关知识点
一、文件存储
1.1、inode (文件属性)
inode为文件是否存在的标志,本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位 置…也叫作文件属性管理结构,大多数的 inode 都存储在磁盘上。少量常用、近期使用的 inode 会被缓存到内存中。
所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。
查看文件信息使用stat命令
stat lseek.c
可以看到Inode编号与连接数Links
1.2、dentry (目录项)
目录项,其本质依然是结构体,重要成员变量有两个 {文件名,inode,…},而文件内 容(data)保存在磁盘盘块中。
1.3、硬链接
由于linux下的文件是通过索引节点(Inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配inode。inode指向了物理硬盘的一个区块,事实上文件系统会维护一个引用计数,只要有文件指向这个区块,它就不会从硬盘上消失。每添加一个硬链接,文件的链接数就加1。
1、创建tmp文件
touch tmp
2、查看链接数:
ls -lrth tmp
,发现链接数为1
ls -lrth tmp
-rw-r--r-- 1 root root 0 Sep 9 16:36 tmp
3、用ln命令来建立硬链接:
ln tmp tmphard
ln tmp tmphard
4、查看链接数与inode:
ls -ilrth tmp*
,发现inode一样,且链接数都是2
ls -ilrth tmp*
1063415 -rw-r--r-- 2 root root 0 Sep 9 16:36 tmphard
1063415 -rw-r--r-- 2 root root 0 Sep 9 16:36 tmp
分析:在创建链接前,tmp显示的链接数目为1,创建链接后对比:
- tmp 和tmphard 的链接数目都变为2;
- tmp 和tmphard 在inode号是一样的;
- tmp 和tmphard 显示的文件大小也是一样;
可见进行了ln命令的操作结果:
tmp 和tmphard 是同一个文件的两个名字
,它们具有同样的索引节点号和文件属性,建立文件tmp的硬链接,就是为tmp的文件索引节点在当前目录上建立一个新指针。
5、删除
你可以删除其中任何一个,每次只会删除一个指针,链接数同时减一,只有将所有指向文件内容的指针,也即链接数减为0时,内核才会把文件内容从磁盘上删除。
rm tmp
ls -ilrth tmp*
1063415 -rw-r--r-- 1 root root 0 Sep 9 16:36 tmphard
硬链接缺点:尽管硬链接节省空间,也是Linux系统整合文件系统的传统方式,但是存在以下不足之处:
- 不可以在不同文件系统的文件间建立链接
电脑与u盘间就不能创建硬链接
- 只有超级用户才可以为目录创建硬链接。
1.4、软链接
软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。
软链接的inode所指向的内容实际上是保存了一个绝对路径,当用户访问这个文件时,系统会自动将其替换成其所指的文件路径。类似于Windows 的快捷方式。
用ln -s命令来建立软链接:
ln -s tmp tmpsoft
ln -s tmp tmpsoft
创建后查看连接数
ls -ilrth tmp*
1063415 -rw-r--r-- 2 root root 0 Sep 9 17:00 tmphard
1063415 -rw-r--r-- 2 root root 0 Sep 9 17:00 tmp
1063416 lrwxrwxrwx 1 root root 3 Sep 9 17:01 tmpsoft -> tmp
如果移除源文件tmp,再查看就会报错
软链接缺点:
- 因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;
- 它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。
二、函数调用
2.1、stat (查看文件信息)
作用:查看文件信息
1、脚本指令stat
查看文件信息使用stat命令
stat tmphard
root@VM-4-5-ubuntu:~/gccTest# stat tmphard
File: tmphard
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fc02h/64514d Inode: 1063415 Links: 2
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-09-09 17:00:40.270413164 +0800
Modify: 2022-09-09 17:00:40.270413164 +0800
Change: 2022-09-09 17:14:08.279544985 +0800
Birth: -
2、通过man page查询stat用法
Man Page第二章是系统调用
man 2 stat
可以看到用法说明,以及struct state的说明
3、函数stat
编程里面也可以通过stat函数获取这些信息
获取文件属性,从inode结构体中获取,同
stat file
函数 | int stat(const char *path, struct stat *buf); |
---|---|
参数 | path: 文件路径 buf:(传出参数) 存放文件属性,inode结构体指针。 |
返回值 | 成功: 0 失败: -1 errno |
应用场景 | 获取文件大小: buf.st_size 获取文件类型: buf.st_mode 获取文件权限: buf.st_mode |
stat 结构体
struct stat
{
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
};
示例
mystat.c
获取文件大小
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
struct stat sbuf;
int ret = stat(argv[1], &sbuf);
if (ret == -1) {
perror("stat eror");
exit(1);
}
printf("file size: %ld\n", sbuf.st_size);
return 0;
}
运行
./a.out lseek.c
file size: 735
2.2、lstat (查看文件信息)
类似于stat,差别是这个不会穿透查询,即可以查询软链接文件
1、lstat用法示例
示例
mylstat.c
判断文件类型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
struct stat sb;
int ret = lstat(argv[1], &sb);
if (ret == -1) {
perror("stat eror");
exit(1);
}
if (S_ISREG(sb.st_mode)) {
printf("It's a regular\n");
} else if (S_ISDIR(sb.st_mode)) {
printf("It's a dir\n");
} else if (S_ISFIFO(sb.st_mode)) {
printf("It's a pipe\n");
} else if (S_ISLNK(sb.st_mode)) {
printf("it's a sym link\n");
}
return 0;
}
运行结果
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmp
It's a regular
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmpsoft
it's a sym link
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmphard
It's a regular
如果是stat,即把lstat这行改为stat
int ret = lstat(argv[1], &sb);
int ret = stat(argv[1], &sb);
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmp
It's a regular
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmphard
It's a regular
root@VM-4-5-ubuntu:~/gccTest# ./a.out tmpsoft
It's a regular
2、穿透符号链接
- stat 会,stat 会拿到符号链接指向那个文件或目录的属性
- 类似穿透现象还有 cat vim(实现基于系统调用)
- lstat 不会,不想穿透符号就用 lstat
文件类型判断方法,使用宏函数:
- S_ISLNK(st_mode):是否是一个连接.
- S_ISREG(st_mode):是否是一个常规文件.
- S_ISDIR(st_mode):是否是一个目录
- S_ISCHR(st_mode):是否是一个字符设备.
- S_ISBLK(st_mode):是否是一个块设备
- S_ISFIFO(st_mode):是否是一个FIFO文件.
- S_ISSOCK(st_mode):是否是一个SOCKET文件
2.3、link && unlink (建立硬链接)
1、回顾ln命令
回顾:用ln命令来建立硬链接:
ln tmp tmphard
ln tmp tmphard
2、系统调用link && unlink
使用ln命令实际上也是调用的系统调用,那么如何通过代码的方式实现创建硬链接
为已经存在的文件创建目录项(硬链接)
函数 | int link(const char *oldpath, const char *newpath); |
---|---|
参数 | oldpath: 旧的路径 newpath:新的路径 |
返回值 | 成功:0 失败:-1 设置 errno 为相应值 |
删除一个文件的目录项
函数 | int unlink(const char *pathname); |
---|---|
参数 | pathname:文件路径 |
返回值 | 成功:0 失败:-1 设置 errno 为相应值 |
3、link && unlink实现改名操作
示例:实现 mv 命令的改名操作
myMv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
link(argv[1], argv[2]);
unlink(argv[1]);
return 0;
}
Linux下删除文件的机制:不断将st_nlink -1,直至减到0为止。无目录项对应的文件,将会被操作系统择机释放。(具体时间由系统内部调度算法决定),
因此,我们删除文件,从某种意义上说,只是让文件具备了被释放的条件。
4、隐式回收概念
当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统 的这一特性称之为隐式回收系统资源。
2.4、getcwd
获取进程当前工作目录
注意:这个是卷 3的标库函数
char *getcwd(char *buf, size_t size);
成功:buf 中保存当前进程工作目录位置。失败返回 NULL。
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
int main(int argc, char *argv[]) {
char path[256];
char *ptr = getcwd(path, sizeof(path));
if (NULL == ptr) {
perror("getcwd error");
exit(-1);
}
printf("Current working directory: %s\n", path);
return 0;
}
运行结果
Current working directory: /Users/zhanglei/CLionProjects/MyCLanguage/cmake-build-debug
2.5、chdir
改变当前进程的工作目录
int chdir(const char *path);
成功:0;失败:-1 设置 errno 为相应值
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
int main(int argc, char *argv[]) {
// set
char path1[256]="/Users/zhanglei/CLionProjects/MyCLanguage";
int result = chdir(path1);
if (result == -1) {
perror("chdir error");
exit(-1);
}
// get
char path[256];
char *ptr = getcwd(path, sizeof(path));
if (NULL == ptr) {
perror("getcwd error");
exit(-1);
}
printf("Current working directory: %s\n", path);
return 0;
}
运行结果
Current working directory: /Users/zhanglei/CLionProjects/MyCLanguage
2.6、文件、目录权限
注意:目录文件也是“文件”。其文件内容是该目录下所有子文件的目录项 dentry。 可以尝试用 vim 打开一个目录。
r | w | x | |
---|---|---|---|
文件 | 文件的内容可以被查看 cat、more、less… | 内容可以被修改 vi、> … | 可以运行产生一个进程 ./文件名 |
目录 | 目录可以被浏览 ls、tree… | 创建、删除、修改文件 mv、touch、mkdir… | 可以被打开、进入 cd |
vim gcctest
2.7、opendir && closedir && readdir
注意:这3个是卷 3的标库函数
1、opendir
根据传入的目录名打开一个目录 (库函数) DIR * 类似于 FILE *
DIR *opendir(const char *name);
成功返回指向该目录结构体指针,失败返回 NULL 参数支持相对路径、绝对路径两种方式:
例如:打开当前目录:
1 getcwd() , opendir()
2 opendir(“.”);
2、closedir
关闭打开的目录
int closedir(DIR *dirp);
成功:0;失败:-1 设置 errno 为相应值
3、readdir
读取目录 (库函数)
struct dirent *readdir(DIR *dirp);
成功,返回目录项结构体指针;失败返回NULL设置errno 为相应值(需注意返回值,读取数据结束时也返回 NULL 值,所以应借助 errno 进一步加以区分)
struct dirent
{
long d_ino; /* inode number 索引节点号 */
off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
unsigned short d_reclen; /* length of this d_name 文件名长 */
unsigned char d_type; /* the type of d_name 文件类型 */
char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长255字符 */
}
由于
\0
结束符暂居一个字符的位置,所以文件名最长254个字符
4、示例:实现简单的 ls 功能
myls.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
DIR * dp;
struct dirent *sdp;
dp = opendir(argv[1]);
if (dp == NULL) {
perror("opendir error");
exit(1);
}
while ((sdp = readdir(dp)) != NULL) {
printf("%s\t", sdp->d_name);
}
printf("\n");
closedir(dp);
return 0;
}
运行结果
root@VM-4-5-ubuntu:~/gccTest# ./a.out ./
.. tmpsoft . lseek2.c mystat.c dynamiclib block_readtty.c mylstat.c nonblock_readtty.c tmp myMv.c tmphard a.out myls.c staticlib gcctest read_cmp_getc.c test.c
5、示例:递归遍历目录
查询指定目录,递归列出目录中文件,同时显示文件大小。
ls-R.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pthread.h>
void isFile(char *name);
// 打开目录读取,处理目录
void read_dir(char *dir, void (*func)(char *)) {
char path[256];
DIR *dp;
struct dirent *sdp;
dp = opendir(dir);
if (dp == NULL) {
perror("opendir error");
return;
}
// 读取目录项
while ((sdp = readdir(dp)) != NULL) {
if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
continue;
}
//fprintf();
// 目录项本身不可访问, 拼接. 目录/目录项
// /mnt/jason a.txt
sprintf(path, "%s/%s", dir, sdp->d_name);
// 判断文件类型,目录递归进入,文件显示名字/大小
//isFile(path);
(*func)(path);
// func(path);//由于函数名称就是个指针,这样写也ok
}
closedir(dp);
}
void isFile(char *name) {
int ret = 0;
struct stat sb;
// 获取文件属性, 判断文件类型
ret = stat(name, &sb);
if (ret == -1) {
perror("stat error");
return;
}
// 是目录文件
if (S_ISDIR(sb.st_mode)) {
read_dir(name, isFile);
}
// 是普通文件, 显示名字/大小
printf("%10s\t\t%lld\n", name, sb.st_size);
}
int main(int argc, char *argv[]) {
// 判断命令行参数
if (argc == 1) {
isFile(".");
} else {
isFile(argv[1]);
}
return 0;
}
运行结果
2.8、重定向
1、命令行重定向
命令行重定向:cat a > b
root@VM-4-5-ubuntu:~/gccTest# cat test.txt
新建文本文档
root@VM-4-5-ubuntu:~/gccTest# cat test.txt > test2.txt
root@VM-4-5-ubuntu:~/gccTest# cat test2.txt
新建文本文档
2、dup
如果使用系统调用,用函数的方式如何实现?
dup:duplicate英文缩写
功能:文件描述符拷贝。 使用现有的文件描述符,拷贝生成一个新的文件描述符,且函数调用前后这个两个文件描述符指向同一文件。
int dup(int oldfd);
成功:返回一个新文件描述符;失败:-1 设置 errno 为相应值
3、dup2
功能:文件描述符拷贝。重定向文件描述符指向。
通过该函数可实现命令行“重定向”功能。使得原来指向某文件的文件描述符,指向其 他指定文件。
int dup2(int oldfd, int newfd);
成功:返回一个新文件描述符;如果 oldfd 有效,则返回的文件描述符与 oldfd 指向同一文件。
失败:如果 oldfd 无效,调用失败,关闭 newfd。返回-1,同时设置 errno 为相应值。
dup2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int fd1 = open(argv[1], O_RDWR); // 012 --- 3
int fd2 = open(argv[2], O_RDWR); // 012 --- 3
int fdret = dup2(fd1, fd2); // 返回 新文件描述符fd2
printf("fdret = %d\n", fdret);
int ret = write(fd2, "1234567", 7); // 写入 fd1 指向的文件
printf("ret = %d\n", ret);
dup2(fd1, STDOUT_FILENO); // 将屏幕输入,重定向给 fd1所指向的文件.
printf("-----------------------------886\n");
return 0;
}
4、fcntl
int fcntl(int fd, int cmd, ....);
cmd: F_DUPFD
当 fcntl 的第二个参数为 F_DUPFD 时, 它的作用是根据一个已有的文件描述符,复制 生成一个新的文件描述符。此时,fcntl 相当于 dup 和 dup2 函数。
参 3 指定为 0 时,因为 0 号文件描述符已经被占用。所以函数自动用一个最小可用文件 描述符。
参 3 指定为 9 时,如果该文件描述符未被占用,则返回 9。否则,返回大于 9 的可用文件描述符。
fcntl 实现dup描述符fcntl_dup.c
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[]){
int fd1 = open(argv[1], O_RDWR);
printf("fd1 = %d\n", fd1);
int newfd = fcntl(fd1, F_DUPFD, 0);
printf("newfd = %d\n", newfd);
return 0;
}
运行结果
root@VM-4-5-ubuntu:~/gccTest# gcc fcntl_dup.c
root@VM-4-5-ubuntu:~/gccTest# ./a.out ls-R.c
fd1 = 3
newfd = 4
回顾open函数
函数 | int open(char *pathname, int flags) |
---|---|
参数 | pathname:要打开的文件路径名 flags:文件打开方式,#include <fcntl.h> |
返回值 | 成功: 打开文件所得到对应的文件描述符(整数) 失败: -1, 设置errno |
这里打开得到的文件描述符为3,fcntl重定向为0,由于0-3都被系统占了,所以输出最小描述符为4