1.文件系统的物理结构
1)硬盘的物理结构
驱动臂、盘片、主轴、磁头、控制器
2)磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。
3)磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
4)柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面
柱面号:确定哪个磁道
扇区号:确定哪个区域
偏移量:确定扇区内的位置若干个连续的柱面构成一个柱面组,若干个连续的柱面组构成一个分区 ,每个分区都建有独立的文件系统,若干分区构成一个磁盘驱动器。
2.文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:| 引导块 | 柱面组 | i节点映射 | 块位图 | i节点表 | 数据块集 | 副 本 | 信 息 | 射 表 | | | |可通过
ls -i
查看i节点号是多少,然后通过这个节点号找出数据块集中的数据
i节点存着文件元数据,文件数据块的位置等根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容
间接块:存储下级文件数据块索引表(这样有利于拓展文件)
3.文件分类
普通文件(-):可执行程序、文本、图片、音频、视频、网页
目录文件(d):该目录中每个硬链接名和i节点号的对应表
ls -i
可查看i节点号和硬链接名符号链接文件(l):存放目标文件的路径
ln -s <源文件> <目标文件>
这样生成的目标文件就链接到源文件了管道文件(p):有名管道,进程间通信
套接字文件(s):进程间通信
块设备文件(b):按块寻址,顺序或随机读写
字符设备文件( c):按字节寻址,只能以字节为单位顺序读写
4.文件的打开与关闭
打开:在系统内核中建立一套数据结构,用于访问文件
关闭:释放打开文件过程中建立的数据结构
打开流程:进程表项->文件描述符表->文件表项->v节点指针(内存中)->v节点->i节点内容open函数
打开已有的文件或创建新文件
#include <fcntl.h>
int open(const char* pathname, int flags, mode_t mode);
成功返回文件描述符,失败返回-1。pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读
O_WRONLY - 只写
O_RDWR - 读写
O_APPEND - 追加
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,有此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,需要有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成(保证写),读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,(缺省即为异步)在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errnomode - 权限模式,三位8进制:0XXX
第一位: 拥有者用户
第二位: 同组用户
第三位: 其它用户
4: 可读
2: 可写
1: 可执行
所创建文件的实际权限除了跟mode参数有关,还受权限掩码(三位8进制)的影响。
权限掩码查看umask
例如:
mode=0666
umask=0002
权限=mode&(~umask)=0664打开已有文件
int open(const char*pathname, int flags);
creat函数
创建新文件
#include <fcntl.h>
int creat(const char* pathname, mode_t mode);
成功返回文件描述符,失败返回-1。
等同open函数的flags: O_WRONLY | O_CREAT | O_TRUNC
close函数
关闭文件
#include <fcntl.h>
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(void) { //可读可写,文件不存在就创建,存在就清空 int fd1 = open("open.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d\n", fd1); int fd2 = open("open.txt", O_RDONLY); if (fd2 == -1) { perror("open"); return -1; } printf("fd2 = %d\n", fd2); close(fd2); close(fd1); return 0; }
作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。
每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项。
每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。
系统内核缺省为每个进程打开三个文件描述符:
#include <unistd.h>
#define STDIN_FILENO 0 // 标准输入,即键盘
#define STDOUT_FILENO 1 // 标准输出,终端屏幕,有缓冲
#define STDERR_FILENO 2 // 标准错误,终端屏幕,无缓冲
文件描述符是用户程序和系统内核关于文件的唯一联系方式。
文件描述符 | UC C | C++ |
---|---|---|
标准输入 0 | stdin | cin |
标准输出 1 | stdout | cout |
标准错误 2 | stderr | cerr |
数据类型 | int FILE* | iostream |
IO重定向操作
通过shell指定
./test 0<in.txt 1>out.txt 2>error.txt
0<in.txt:将标准输入重定向到in.txt
1>out.txt:将标准输出重定向到out.txt
2>error.txt:将标准错误输出重定向到error.txt
重定向实现逻辑代码如下:void redir(void) { //先关闭默认开启的标准输入,此时文件描述符0就空出来了 close(STDIN_FILENO); //再开启in.txt文件,其文件描述符0就被它占用到了 creat("in.txt", 0644); //先关闭默认开启的标准输出,此时文件描述符1就空出来了 close(STDOUT_FILENO); //再开启out.txt文件,其文件描述符1就被它占用到了 creat("out.txt", 0644); //先关闭默认开启的标准错误输出,此时文件描述符2就空出来了 close(STDERR_FILENO); //再开启error.txt文件,其文件描述符2就被它占用到了 creat("error.txt", 0644); }
恢复文件描述符与标准IO的绑定实现代码
void resume(void) { //先把文件描述符0解绑 close(STDIN_FILENO); //将文件描述符0与标准输入绑定,/dev/tty为标准IO,只读打开为键盘输入 stdin = fopen("/dev/tty", "r"); //先把文件描述符1解绑 close(STDOUT_FILENO); //将文件描述符1与标准输出绑定,/dev/tty为标准IO,只写打开为屏幕输出 stdout = fopen("/dev/tty", "w"); //先把文件描述符2解绑 close(STDERR_FILENO); //将文件描述符2与标准输出绑定,/dev/tty为标准IO,只写打开为屏幕输出 stderr = fopen("/dev/tty", "w"); //标准输出和标准错误输出的差别在于标准错误输出没有缓存区的,这里要把缓存取消掉 setbuf(stderr, NULL); } ```
5.文件的读取和写入
write函数
向指定文件写入字节流
ssize_t write(int fd, const void* buf, size_t count);
ssize:有符号整型,第一个s为signed
成功返回实际写入的字节数(0表示未写入),失败返回-1。fd -文件描述符
buf - 内存缓冲区
count - 期望写入的字节数
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("write.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } const char* text = "Hello, World!"; printf("写入内容:%s\n", text); size_t towrite = strlen(text) * sizeof( text[0]); ssize_t written = write(fd, text, towrite); if (written == -1) { perror("write"); return -1; } printf("期望写入%u字节,实际写入%d字节。\n", towrite, written); close(fd); return 0; } ```
read函数
ssize_t read(int fd, void* buf, size_t count);
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1。
ABCDEFGHIJLMNOPQ 4
1)ABCD 2)EFGH 3)IJLM 4)NOPQ 5)->0fd - 文件描述符
buf - 内存缓冲区
count - 期望读取的字节数
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("read.txt", O_RDONLY); if (fd == -1) { perror("open"); return -1; } char text[256]; //最后一个是空字符,所以最大可读取的字节数要在原大小下-1 size_t toread = sizeof(text) - sizeof( text[0]); ssize_t readed = read(fd, text, toread); if (readed == -1) { perror("read"); return -1; } printf("期望读取%u字节,实际读取%d字节。\n", toread, readed); //最后添加空字符 text[readed / sizeof(text[0])] = '\0'; printf("读取内容:%s\n", text); close(fd); return 0; }
memset函数
#include <string.h>
void *memset(void *s, int c, unsigned long n);函数的功能是:将指针变量 s 所指向的前 n 字节的内存单元用一个“整数” c 替换,注意 c 是 int 型。
s 是 void*型的指针变量,所以它可以为任何类型的数据进行初始化。
memset一般使用“0”初始化内存单元,而且通常是给数组或结构体进行初始化。
memset是对较大的数组或结构体进行清零初始化的最快方法,因为它是直接对内存进行操作的。
sprintf函数
#include <stdio.h>
int sprintf(char *str, const char *format, …)
如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数。函数功能:发送格式化输出到 str 所指向的字符串。
str – 这是指向一个字符数组的指针,该数组存储了 C 字符串。
format – 这是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。
#include <stdio.h> #define PI 3.14159 int main() { char str[80]; sprintf(str, "Pi 的值 = %f", PI); puts(str); return 0; }
sscanf函数
#include <stdio.h>
int sscanf(const char *str, const char *format, …);
如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。函数功能:从字符串读取格式化输入。
str – 这是 C 字符串,是函数检索数据的源。
format – 这是 C字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符。
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { int day, year; char weekday[20], month[20],dtm[100]; strcpy( dtm, "Saturday March 25 1989" ); sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year ); printf("%s %d, %d = %s\n", month, day, year, weekday ); return(0); }
文本文件的可读性
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读的,那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
//写出的文本不可读 #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("binary.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } char name[256] = "张飞"; if (write(fd, name, sizeof(name)) == -1) { perror("write"); return -1; } unsigned int age = 38; if (write(fd, &age, sizeof(age)) == -1) { perror("write"); return -1; } double salary = 20000; if (write(fd, &salary, sizeof(salary)) == -1) { perror("write"); return -1; } struct Employee { char name[256]; unsigned int age; double salary; }employee = {"赵云", 25, 10000}; if (write(fd, &employee, sizeof( employee)) == -1) { perror("write"); return -1; } close(fd); if ((fd = open("binary.dat", O_RDONLY)) == -1) { perror("open"); return -1; } if (read(fd, name, sizeof(name)) == -1) { perror("read"); return -1; } printf("姓名:%s\n", name); if (read(fd, &age, sizeof(age)) == -1) { perror("read"); return -1; } printf("年龄:%u\n", age); if (read(fd, &salary, sizeof( salary)) == -1) { perror("read"); return -1; } printf("工资:%g\n", salary); if (read(fd, &employee, sizeof( employee)) == -1) { perror("read"); return -1; } printf("员工:%s, %u, %g\n", employee.name, employee.age, employee.salary); close(fd); return 0; }
//写出的文本可读 #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } char name[256] = "张飞"; unsigned int age = 38; double salary = 20000; char buf[1024]; sprintf(buf, "%s %u %.2lf\n", name, age, salary); if (write(fd, buf, strlen(buf) * sizeof( buf[0])) == -1) { perror("write"); return -1; } struct Employee { char name[256]; unsigned int age; double salary; } employee = {"赵云", 25, 10000}; sprintf(buf, "%s %u %.2lf\n", employee.name, employee.age, employee.salary); if (write(fd, buf, strlen(buf) * sizeof( buf[0])) == -1) { perror("write"); return -1; } close(fd); if ((fd = open("text.txt", O_RDONLY)) == -1) { perror("open"); return -1; } //字符数组buf全部清零,这样后面数据读入无需再加'\0'字符了 memset(buf, 0, sizeof(buf)); if (read(fd, buf, sizeof(buf) - sizeof( buf[0])) == -1) { perror("read"); return -1; } sscanf(buf, "%s%u%lf%s%u%lf", name, &age, &salary, employee.name, &employee.age, &employee.salary); printf("姓名:%s\n", name); printf("年龄:%u\n", age); printf("工资: %g\n", salary); printf("员工:%s, %u, %g\n", employee.name, employee.age, employee.salary); close(fd); return 0; }
6.文件顺序与随机读写
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。
打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。 因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
lseek函数
人为调整文件读写位置
#include <unistd.h>
#include <sys/types.h>
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,也就是距离文件开头多少个字节,失败返回-1。fd - 文件描述符
offset - 文件读写位置相对于whence参数的偏移量
whence - 可有以下标记:
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);//从文件头往后10个字节的位置
lseek(fd, -10, SEEK_END); //从文件尾往前10个字节的位置
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("seek.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } const char* text = "Hello,World!"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } if (lseek(fd, -6, SEEK_CUR) == -1) { perror("lseek"); return -1; } off_t pos = lseek(fd, 0, SEEK_CUR); if (pos == -1) { perror("lseek"); return -1; } printf("当前读写位置:%ld\n", pos); // 6 //将Linux替换掉了World,文件游标指向了 text = "Linux"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } // 文件的读写位置可以在文件尾之后 if (lseek(fd, 8, SEEK_END) == -1) { perror("lseek"); return -1; } text = "<-这里有个洞洞!"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } off_t size = lseek(fd, 0, SEEK_END); if (size == -1) { perror("lseek"); return -1; } printf("文件大小:%ld\n", size); // 文件的读写位置不能在文件头之前 // if (lseek(fd, -8, SEEK_SET) == -1) { /*if (lseek(fd, -size-1, SEEK_END) == -1) { perror("lseek"); return -1; }*/ close(fd); return 0; }
可再终端用
hexdump -C xxx.txt
查看文本的十六进制
7.标准I/O和系统I/O
缓冲区优化
调用过程:
应用程序 --> 标准(库)I/O(fopen/fwrite/fclose) --> 系统(库)I/O(open/write/close)
标准库通过缓冲区优化,减少系统调用的次数,降低在用户态和内核态之间来回切换的频率,提高运行速度,缩短运行时间。(即可通过把数据暂缓放在缓冲区后等其满后再调用系统IO,便减少了系统IO的调用次数,从而减少了用户态与内核态的切换时间了)//系统IO调用效率测试代码 #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("sysio.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } for (unsigned int i = 0; i < 10000000; ++i) write(fd, &i, sizeof(i)); close(fd); return 0; }
//标准IO调用效率测试代码 #include <stdio.h> int main(void) { FILE* fp = fopen("sysio.dat", "wb"); if (!fp) { perror("fopen"); return -1; } //setbuf(fp, NULL);//可把缓冲区除掉进行对比 for (unsigned int i = 0; i < 10000000; ++i) fwrite(&i, sizeof(i), 1, fp); fclose(fp); return 0; }
两者时间对比如下:
8.复制(duplicate)文件描述符(表项)
dup函数
int dup(int oldfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项所对应的文件描述符。
例如:
int fd1 = creat(“text.txt”,0644);
int fd2 = dup(fd1); // fd2等于文件描述表项中最小的未被分配到的文件描述符
这样fd1和fd2对应同一个文件表项,访问同一个文件。
close(fd1);//此时这个文件描述符没清理了,但还可以通过fd2访问到该文件
close(fd2);//此时描述该文件的文件描述符全被清除了,该文件不可访问了
dup2函数
int dup2(int oldfd, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符dup2函数可自行指定文件描述符。在复制oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,再行复制。
重复使用open和单次使用dup的区别
fd1 = open(“1.txt”, …);
fd2 = open(“1.txt”, …);
这样fd1和fd2有各自的文件表项,但共同指向同一个v节点,因为都是维护1.txt这个文件。因为有各自独立的文件表项,所以它们的读写位置也是独立的,互不干扰。fd1 = open(“2.txt”, …);
fd2 = dup(fd1);
这样fd1和fd2的文件表项是同一个,自然指向同一个v节点,都是维护2.txt这个文件 。因为两个文件描述符的文件表项是同一个,所以它们的读写位置是一致的,改变其一,另外的也会改变。//共享读写位置 #include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> int main(void) { int fd1 = open("dup.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d\n", fd1); int fd2 = dup(fd1); if (fd2 == -1) { perror("dup"); return -1; } printf("fd2 = %d\n", fd2); int fd3 = dup2(fd2, 100); if (fd3 == -1) { perror("dup2"); return -1; } printf("fd3 = %d\n", fd3); const char* text = "Hello, World!"; if (write(fd1, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } if (lseek(fd2, -6, SEEK_CUR) == -1) { perror("lseek"); return -1; } text = "Linux"; if (write(fd3, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } close(fd3); close(fd2); close(fd1); return 0; }
//独立读写位置 #include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> int main(void) { int fd1 = open("same.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d\n", fd1); int fd2 = open("same.txt", O_RDWR); if (fd2 == -1) { perror("dup"); return -1; } printf("fd2 = %d\n", fd2); int fd3 = open("same.txt", O_RDWR); if (fd3 == -1) { perror("dup2"); return -1; } printf("fd3 = %d\n", fd3); const char* text = "Hello, World!"; if (write(fd1, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } if (lseek(fd2, -6, SEEK_END) == -1) { perror("lseek"); return -1; } text = "Linux"; if (write(fd3, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } close(fd3); close(fd2); close(fd1); return 0; }
9.文件控制
fcntl函数
功能描述:根据文件描述词来操作文件的特性。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, …);
返回值:与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。
PS:在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
fcntl函数有5种功能:
①复制一个现有的描述符(cmd=F_DUPFD)
int fcntl(int oldfd, F_DUPFD, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
该函数类似dup2函数,但略有不同。如果newfd处于打开状态,该函数并不会象dup2函数那样关闭它,而是另外寻找一个比它大的最小的空闲文件描述符作为复制目标。#include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> int main(void) { int fd1 = open("fcntl1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d\n", fd1); int fd2 = open("fcntl2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd2 == -1) { perror("open"); return -1; } printf("fd2 = %d\n", fd2); //dup2和fcntl进行对比 //int fd3 = dup2(fd1, fd2); 此时fd1、fd2和fd3指向同一个文件 int fd3 = fcntl(fd1, F_DUPFD, fd2);//由于fd2存在了, //所以这条语句只有fd1和fd3指向同一个文件, //fd2指向另一个文件 if(fd3 == -1) { perror("dup2"); return -1; } printf("fd3 = %d\n", fd3); const char* text = "123"; if (write(fd1, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } text = "456"; if (write(fd2, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } text = "789"; if (write(fd3, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return -1; } close(fd3); close(fd2); close(fd1); return 0; }
②获得/追加设置文件描述符标记(cmd=F_GETFD或F_SETFD).
截止目前只有一个文件描述符标志位:FD_CLOEXEC
一个进程可以通过exec函数族启动另一个进程取代其自身。
原进程中无FD_CLOEXEC标志位的文件描述符在新进程中会依然保持打开状态,这也是文件描述符的默认状态。如果原进程中的某个文件描述符带有此标志位,那么在新进程中该文件描述符会被关闭。int fcntl(int fd, F_GETFD);
成功返回文件描述符标志,失败返回-1。int fcntl(int fd, F_SETFD, int flags);
成功返回0,失败返回-1。#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(void) { int fd = open("fd.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } int flags = fcntl(fd, F_GETFD); if (flags == -1) { perror("fcntl"); return -1; } again: if (flags & FD_CLOEXEC)//&用来做检测,因为FD_CLOEXEC这类只标志一位(8、4、2、1) printf("文件描述符%d" "将在新进程中被关闭。\n", fd); else { printf("文件描述符%d" "将在新进程中保持打开。\n", fd); //设置FD_CLOEXEC标志位,这里开启新进程时把旧进程的文件标识符给关了的操作 flags |= FD_CLOEXEC; if (fcntl(fd, F_SETFD, flags) == -1) { perror("fcntl"); return -1; } goto again;//在检测一次 } // 调用exec函数族创建新进程... //...... //...... close(fd); return 0; }
③获得/追加设置文件状态标记(cmd=F_GETFL或F_SETFL)
int fcntl(int fd, F_GETFL);
成功返回文件状态标志,失败返回-1。
与文件创建有关的三个状态标志:O_CREAT/O_EXCL/O_TRUNC,无法被获取。PS:只读标志O_RDONLY的值为0,不能用位与(&)检测。
int flags = fcntl(fd, F_GETFL);
//ACCMODE为只有读写标志位的掩码,所以这里先把读写的标志位给留下来
if ((flags & O_ACCMODE) == O_RDONLY)
// 只读文件
其他的直接用位与检测即可:
if (flags & O_WRONLY)
// 只写文件
if (flags & O_RDWR)
// 读写文件int fcntl(int fd, F_SETFL, flags);
成功返回0,失败返回-1。
只有O_APPEND和O_NONBLOCK两个状态标志可被追加。#include <stdio.h> #include <unistd.h> #include <fcntl.h> void pflags(int flags) { //用16进制打印传入的标志位 printf("文件状态标志(%#X):", flags); struct { int flag; const char* desc; } flist[] = { {O_RDONLY, "O_RDONLY"}, {O_WRONLY, "O_WRONLY"}, {O_RDWR, "O_RDWR"}, {O_APPEND, "O_APPEND"}, {O_CREAT, "O_CREAT"}, {O_EXCL, "O_EXCL"}, {O_TRUNC, "O_TRUNC"}, {O_NOCTTY, "O_NOCTTY"}, {O_NONBLOCK, "O_NONBLOCK"}, {O_SYNC, "O_SYNC"}, {O_DSYNC, "O_DSYNC"}, {O_RSYNC, "O_RSYNC"}, {O_ASYNC, "O_ASYNC"}}; //遍历检测传入的参数为哪种标识符 for (size_t i = 0; i < sizeof(flist) / sizeof(flist[0]); ++i) { //对只读标志进行特定处理 if (flist[i].flag == O_RDONLY) { if ((flags & O_ACCMODE) == flist[i].flag) printf("%s ", flist[i].desc); } else if (flags & flist[i].flag) printf("%s ", flist[i].desc); } printf("\n"); } int main(void) { int fd = open("fl.txt", O_RDONLY | O_CREAT | O_TRUNC | O_ASYNC, 0644); if (fd == -1) { perror("open"); return -1; } int flags = fcntl(fd, F_GETFL); if (flags == -1) { perror("fcntl"); return -1; } pflags(flags); //这里追加了三个标志位,但O_RDWR无效, //因为与前面打开文件时的O_RDONLY冲突了,但不会报错 if (fcntl(fd, F_SETFL, O_RDWR | O_APPEND | O_NONBLOCK) == -1) { perror("fcntl"); return -1; } if ((flags = fcntl(fd, F_GETFL)) == -1) { perror("fcntl"); return -1; } pflags(flags); close(fd); return 0; }
④获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
F_GETOWN 取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)
F_SETOWN 设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程组id通过提供负值的arg来说明,否则,arg将被认为是进程id
⑤获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
int fcntl(int fd, F_SETLKW/F_SETLK, struct flock* lock);
F_SETLKW - 阻塞
F_SETLK - 非阻塞具体看第10点的文件锁
struct flock {
short int l_type; // 锁类型:F_RDLCK 、F_WRLCK 、 F_UNLCK
short int l_whence; // 锁区偏移起点:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; // 锁区偏移
off_t l_len; // 锁区长度(字节数),0表示锁到尾
pid_t l_pid; // 加锁进程PID,-1表示自动设置
};
10.文件锁
为了解决读写冲突问题(不希望多个进程对同一个文件进行读写,特别是写操作)
文件访问情况 | 读取 | 写入 |
---|---|---|
无人访问 | OK | OK |
多人在读 | OK | NO |
一人在写 | NO | NO |
为了避免在读写同一个文件的同一个区域时发生冲突,进程之间应该遵循以下规则(读共享,写独占):
- 如果一个进程正在写,那么其它进程既不能写也不能读。
- 如果一个进程正在读,那么其它进程不能写但是可以读。
为了避免多个进程在读写同一个文件的同一个区域时发生冲突,操作系统引入了文件锁机制,并把文件锁分读锁和写锁两种,它们区别在于:
读锁:共享锁,对一个文件的特定区域可以同时加多个读锁
写锁:排它锁,对一个文件的特定区域只能加一把写锁
锁模式:加锁->读写->解锁
加锁情况 | 读锁 | 写锁 |
---|---|---|
无任何锁 | OK | OK |
多把读锁 | OK | NO |
一把写锁 | NO | NO |
设置记录锁fcntl函数(cmd=F_SETLK或F_SETLKW)
int fcntl(int fd, F_SETLKW/F_SETLK, struct flock* lock);
成功返回0,失败返回-1。
F_SETLKW - 阻塞
F_SETLK - 非阻塞struct flock {
short int l_type; // 锁类型:F_RDLCK读锁 、F_WRLCK写锁 、 F_UNLCK解锁
short int l_whence; // 锁区偏移起点:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; // 锁区偏移
off_t l_len; // 锁区长度(字节数),0表示锁到尾
pid_t l_pid; // 加锁进程PID,-1表示自动设置
};举例:
//对相对于文件头10字节开始的20字节以阻塞模式加读锁。 //这里如果锁区先前有被锁过的区域,则会发生阻塞,直到先前被锁区域解锁才轮到 struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = 10; lock.l_len = 20; lock.l_pid = -1; fcntl(fd, F_SETLKW, &lock);
//对相对于当前位置10字节开始到文件尾以非阻塞方式加写锁。 struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = SEEK_CUR; lock.l_start = 10; lock.l_len = 0; lock.l_pid = -1; fcntl(fd, F_SETLK, &lock);
//对整个文件解锁。 struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; lock.l_pid = -1; fcntl(fd, F_SETLK, &lock);
写锁操作代码:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> // 加写锁 int wlock(int fd, int wait) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 解除锁 int ulock(int fd) { struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "用法:%s <字符串>\n", argv[0]); return -1; } int fd = open("shared.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd == -1) { perror("open"); return -1; } /*以阻塞的方式实现写锁 if (wlock(fd, 1) == -1) { perror("wlock"); return -1; } */ //以非阻塞的方式实现写锁,并在锁的时候实现空闲处理 while (wlock(fd, 0) == -1) { if (errno != EACCES && errno != EAGAIN) {//确定是因为有锁导致的错误号,如果不是则输出错误原因 perror("wlock"); return -1; } printf("该文件已被锁定,稍后再试...\n"); // 空闲处理... } //写锁正常被调用时,便可以写数据了 size_t len = strlen(argv[1]); for (size_t i = 0; i < len; ++i) { if (write(fd, &argv[1][i], sizeof( argv[1][i])) == -1) { perror("write"); return -1; } sleep(1); } //写完数据,解锁,以便文件上其他锁 if (ulock(fd) == -1) { perror("ulock"); return -1; } close(fd); return 0; }
读锁操作代码:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> // 加读锁 int rlock(int fd, int wait) { struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 解除锁 int ulock(int fd) { struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main(void) { int fd = open("shared.txt", O_RDONLY); if (fd == -1) { perror("open"); return -1; } /*以阻塞的方式实现读锁 if (rlock(fd, 1) == -1) { perror("rlock"); return -1; } */ //以非阻塞的方式实现读锁,并在锁的时候实现空闲处理 while (rlock(fd, 0) == -1) { if (errno != EACCES && errno != EAGAIN) {//确定是因为有锁导致的错误号,如果不是则输出错误原因 perror("rlock"); return -1; } printf("该文件已被锁定,稍后再试...\n"); // 空闲处理... } char buf[1024]; ssize_t readed; while ((readed = read(fd, buf, sizeof( buf))) > 0) write(STDOUT_FILENO, buf, readed); /*改为慢速读取(一个字节一个字节的读),检验两进程同时读时有没有锁 char buf[1]; ssize_t readed; while ((readed = read(fd, buf, sizeof( buf))) > 0) { write(STDOUT_FILENO, buf, readed); sleep(1); } */ printf("\n"); if (readed == -1) { perror("read"); return -1; } if (ulock(fd) == -1) { perror("ulock"); return -1; } close(fd); return 0; }
获得记录锁fcntl函数(cmd=F_GETLK)
int fcntl(int fd, F_GETLK, struct flock* lock);
成功返回0,失败返回-1。测试对文件的某个区域是否可以加某种锁,如果不能加锁,是什么原因导致加锁冲突?(通过lock这个参数分析)
调用该函数时,lock参数表示欲加之锁的细节。函数成功返回时,通过lock参数输出欲加之锁是否可加,以及存在冲突的锁信息。
进程1进行读写加锁:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> // 加读锁(区域加锁) int rlock(int fd, off_t start, off_t len, int wait) { struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 加写锁(区域加锁) int wlock(int fd, off_t start, off_t len, int wait) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 解除锁(区域加锁) int ulock(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main(void) { printf("进程标识(PID):%d\n", getpid()); int fd = open("shared.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return -1; } const char* text = "ABCDEFGHIJKLMNOPQRST"; if (write(fd, text, strlen(text) * sizeof( text[0])) == -1) { perror("write"); return 0; } printf("对EFGH加读锁"); if (rlock(fd, 4, 4, 0) == -1) { printf("失败:%m\n"); return -1; } printf("成功!\n"); printf("对MNOP加写锁"); if (wlock(fd, 12, 4, 0) == -1) { printf("失败:%m\n"); return -1; } printf("成功!\n"); printf("按<回车>,解锁MN..."); getchar();//让别的进程等待,直至解锁 ulock(fd, 12, 2); printf("按<回车>,解锁EFGH..."); getchar(); ulock(fd, 4, 4); close(fd); return 0; }
进程2进行读写测试:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> // 打印锁(根据lock结构体得到锁的详细信息) void plock(struct flock lock) { if (lock.l_type == F_UNLCK) printf("没有锁。\n"); else { printf("%d进程", lock.l_pid); switch (lock.l_whence) { case SEEK_SET: printf("在距文件头"); break; case SEEK_CUR: printf("在距当前位置"); break; case SEEK_END: printf("在距文件尾"); break; } printf("%ld字节处,为%ld字节加了", lock.l_start, lock.l_len); switch (lock.l_type) { case F_RDLCK: printf("读锁。\n"); break; case F_WRLCK: printf("写锁。\n"); break; } } } // 测读锁(看看锁的情况) int rtest(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; if (fcntl(fd, F_GETLK, &lock) == -1) return -1; plock(lock); return 0; } // 测写锁(看看锁的情况) int wtest(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; if (fcntl(fd, F_GETLK, &lock) == -1) return -1; plock(lock); return 0; } // 加读锁 int rlock(int fd, off_t start, off_t len, int wait) { struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 加写锁 int wlock(int fd, off_t start, off_t len, int wait) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } // 解除锁 int ulock(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main(void) { int fd = open("shared.txt", O_RDWR); if (fd == -1) { perror("open"); return -1; } printf("对CDEF测读锁。"); if (rtest(fd, 2, 4) == -1) { printf("失败:%m\n"); return -1; } printf("对CDEF加读锁"); if (rlock(fd, 2, 4, 0) == -1) printf("失败:%m\n"); else { printf("成功!\n"); ulock(fd, 2, 4); } printf("对CDEF测写锁。"); if (wtest(fd, 2, 4) == -1) { printf("失败:%m\n"); return -1; } printf("对CDEF加写锁"); if (wlock(fd, 2, 4, 0) == -1) printf("失败:%m\n"); else { printf("成功!\n"); ulock(fd, 2, 4); } printf("对KLMN测读锁。"); if (rtest(fd, 10, 4) == -1) { printf("失败:%m\n"); return -1; } printf("对KLMN加读锁"); if (rlock(fd, 10, 4, 0) == -1) printf("失败:%m\n"); else { printf("成功!\n"); ulock(fd, 10, 4); } printf("对KLMN测写锁。"); if (wtest(fd, 10, 4) == -1) { printf("失败:%m\n"); return -1; } printf("对KLMN加写锁"); if (wlock(fd, 10, 4, 0) == -1) printf("失败:%m\n"); else { printf("成功!\n"); ulock(fd, 10, 4); } printf("等待KLMN上的写锁被解除...\n");//这里有换行符会直接输出 printf("对KLMN加写锁");//这里没有换行符不会直接输出,下面以阻塞的方式写锁,等到解锁后遇到换行符才一并输出 if (wlock(fd, 10, 4, 1) == -1) printf("失败:%m\n"); else { printf("成功!\n"); ulock(fd, 10, 4); } close(fd); return 0; }
v节点内有: i节点内容、锁表指针–>锁节点–>锁节点–>…锁的类型、锁区偏移、锁区大小、加锁进程的PID 等
每次对给定文件的特定区域加锁,都会通过fcntl函数向系统内核传递flock结构,该结构中包含了有关锁的一些细节,诸如锁的类型、锁区的起始位置和大小,甚至加锁进程的PID(填-1由系统自动设置)。系统内核会收集所有进程对该文件所加的各种锁,并把这些flock结构中的信息,以链表的形式组织成一张锁表,其起始地址就保存在该文件的v节点中。任何一个进程通过fcntl函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现有与欲加之锁构成冲突的锁即阻塞或报错,否则即将欲加之锁插入锁表,而解锁的过程实际上就是调整或删除锁表中的相应节点。
文件锁属于劝谏锁,亦称协议锁。
11.文件元数据
i节点包含文件元数据、 数据块索引表等。
元数据的结构(通用)如下: struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // i节点号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // 特殊设备ID
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; // 最后状态改变时间(元数据中任何参数发生改变都会随之改变)
};
文件类型和权限的数据类型mode_t其实就是一个整数,其中只有低16位有效(ls -l可查看标识
)。
通过掩码:S_IFMT(0xf000)可解析出高四位
B15~B12、宏名 | 文件类型 |
---|---|
1000、S_IFREG | 普通文件( - ) |
0100、S_IFDIR | 目录文件( d ) |
1100、S_IFSOCK | 本地套接字文件( s ) |
0010、S_IFCHR | 字符设备文件( c ) |
0110、S_IFBLK | 块设备文件( b ) |
1010、S_IFLNK | 符号链接文件( l ) |
0001、S_IFFIFO | 有名管道文件( p ) |
B11~B9 - 设置用户ID位、设置组ID位和粘滞位。
带有设置用户ID位(即B11位为1)的可执行文件(如:/usr/bin/passwd):系统中的每个进程其实都有两个用户ID,一个叫实际用户ID,取决于运行该进程的登录用户,另一个叫有效用户ID。一般情况下,进程的有效用户ID就取自其实际用户ID。但是如果产生该进程的可执行文件带有设置用户ID位,那么该进程的有效用户ID就不再取自实际用户ID,而是取自该可执行文件的拥有者用户ID。进程对系统资源的权限判定是根据其有效用户ID做出的,因此通过这种方法,就可以提升普通用户执行进程的权限,完成本来只有高权限用户才能完成的任务,即有限提权。
带有设置用户ID位的不可执行文件:毫无意义。
chmod u+s xxx
命令可设置用户ID位带有设置组ID位(即B10位为1)的可执行文件: 设置组ID位(B10)的情况与上述类似,只是针对进程的有效组ID而已。
带有设置组ID位的不可执行文件:某些系统用这种无意义的状态作为强制锁标志。
chmod g+s xxx
命令可设置组用户ID位带有粘滞位(B9位为1)的目录:除root以外的任何用户在该目录下,都只能删除或更名那些属于自己的文件或子目录,而对于其它用户的文件或子目录,既不能删除也不能改名。
如:/tmp
B8B7B6 - 对拥有者用户设置读、写、执行的权限
B5B4B3 - 拥有者组设置读、写、执行的权限
B2B1B0 - 其它用户设置读、写、执行的权限
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
-/r | -/w | -/x | -/r | -/w | -/x | -/r | -/w | -/x |
用户ID位时标识S/s | 组ID位时标识S/s | 粘滞位标识T/t |
如下图的passwd文件:
stat函数
#include <sys/stat.h>
int stat(char const* path, struct stat* buf); //跟踪符号链接
成功返回0,失败返回-1。
path - 文件路径
buf - 文件元数据结构
fstat函数
#include <sys/stat.h>
int fstat(int fd, struct stat* buf); //如果fd为符号链接文件,这个会跟踪符号链接到的文件的数据
成功返回0,失败返回-1。
fd - 文件描述符
buf - 文件元数据结构
lstat函数
#include <sys/stat.h>
int lstat(char const* path, struct stat* buf); //不跟踪符号链接
成功返回0,失败返回-1。
path - 文件路径
buf - 文件元数据结构
#include <stdio.h> #include <string.h> #include <sys/stat.h> #include <time.h> const char* mode_to_string(mode_t m) { static char s[11]; mode_t type = m & S_IFMT;//将高四位留下,其为文件类型的掩码 if(type == S_IFDIR) strcpy(s,"d"); else if(type == S_IFLNK) strcpy(s,"l"); else if(type == S_IFBLK) strcpy(s,"b"); else if(type == S_IFCHR) strcpy(s,"c"); else if(type == S_IFSOCK) strcpy(s,"s"); else if(type == S_IFIFO) strcpy(s,"p"); else strcpy(s,"-"); strcat(s,m & S_IRUSR ? "r" : "-");//S_IRUSR为用户ID的可读掩码 strcat(s,m & S_IWUSR ? "w" : "-");//S_IWUSR为用户ID的可写掩码 strcat(s,m & S_IXUSR ? "x" : "-");//S_IXUSR为用户ID的可执行掩码 strcat(s,m & S_IRGRP ? "r" : "-");//S_IRGRP为组用户ID的可读掩码 strcat(s,m & S_IWGRP ? "w" : "-");//S_IWGRP为组用户ID的可写掩码 strcat(s,m & S_IXGRP ? "x" : "-");//S_IXGRP为组用户ID的可执行掩码 strcat(s,m & S_IROTH ? "r" : "-");//S_IROTH为其他用户ID的可读掩码 strcat(s,m & S_IWOTH ? "w" : "-");//S_IWOTH为其他用户ID的可写掩码 strcat(s,m & S_IXOTH ? "x" : "-");//S_IXOTH为其他用户ID的可执行掩码 if (m & S_ISUID) //S_ISUID为用户ID位的掩码 s[3] = (s[3] == 'x' ? 's' : 'S');//指定位置改变标志 if (m & S_ISGID) //S_ISGID为组用户ID位的掩码 s[6] = (s[6] == 'x' ? 's' : 'S'); if (m & S_ISVTX) //S_ISVTX为粘滞位的掩码 s[9] = (s[9] == 'x' ? 't' : 'T'); return s; } const char* time_to_string(time_t t) { static char s[20]; struct tm* local = localtime(&t);//标准C库里的函数,实现秒转化为年月日时分秒,存放于结构体中 sprintf(s, "%04d-%02d-%02d %02d:%02d:%02d", local->tm_year + 1900,//年份从1900年开始算,所以先加上1900 local->tm_mon + 1,//月份从0开始,所以加1 local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec); return s; } int main(int argc, char* argv[]) { if (argc < 2) goto usage; struct stat st; if (argc < 3) { // 跟踪符号链接 if (stat(argv[1], &st) == -1) { perror("stat"); return -1; } } else if (!strcmp(argv[2], "-l")) { // 不跟踪符号链接 if (lstat(argv[1], &st) == -1) { perror("lstat"); return -1; } } else goto usage; printf("设备ID:%lu\n", st.st_dev); printf("i节点号:%ld\n", st.st_ino); printf("模式:%s\n", mode_to_string(st.st_mode));//十进制数通过自定义函数转化为字符串,便于查看 printf("硬链接数:%lu\n", st.st_nlink); printf("用户ID:%d\n", st.st_uid); printf("组ID:%d\n", st.st_gid); printf("特殊设备ID:%lu\n", st.st_rdev); printf("总字节数:%ld\n", st.st_size); printf("I/O块字节数:%ld\n", st.st_blksize); printf("占用块数:%ld\n", st.st_blocks); printf("最后访问时间:%s\n", time_to_string(st.st_atime));//将得到的时间(只有秒数据)转化为有可读性的字符串 printf("最后修改时间:%s\n", time_to_string(st.st_mtime)); printf("最后改变时间:%s\n", time_to_string(st.st_ctime)); return 0; usage: fprintf(stderr, "用法:%s <文件> [-l]\n", argv[0]); return -1; }
12.访问测试
access函数
#include <sys/stat.h>
int access(const char* pathname, int mode);
成功返回0,失败(不可访)返回-1。pathname - 文件路径
mode - 访问权限,可取以下值:
R_OK: 可读否
W_OK: 可写否
X_OK: 可执行否
F_OK: 存在否根据调用该函数的进程的实际用户ID和实际组ID,检测其是否可读、可写或可执行给定的文件,也可检测该文件是否存在。
#include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "用法:%s <文件>\n", argv[0]); return -1; } printf("文件%s", argv[1]); if (access(argv[1], F_OK) == -1) printf("不存在(%m)。\n"); else { if (access(argv[1], R_OK) == -1) printf("不可读(%m),"); else printf("可读,"); if (access(argv[1], W_OK) == -1) printf("不可写(%m),"); else printf("可写,"); if (access(argv[1], X_OK) == -1) printf("不可执行(%m)。\n"); else printf("可执行。\n"); } return 0; }
13.权限掩码
umask函数
#include <sys/stat.h>
mode_t umask(mode_t cmask);
永远成功,返回原来的权限掩码。cmask - 新权限掩码
权限掩码是进程的属性之一,存储在系统内核中的进程表项里。umask函数所影响的仅仅是调用进程自己,对于其它进程,包括其父进程,如Shell,都没有任何影响。
shell 默认的权限掩码为0002,可通过
umask
在终端查看#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main(void) { int fd = open("umask1.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); return -1; } close(fd); mode_t old = umask(0222);//改变了调用进程本身的权限属性,接下来进行文件权限设置时写操作被屏蔽掉了 if ((fd = open("umask2.txt", O_RDWR | O_CREAT | O_TRUNC, 0666)) == -1) { perror("open"); return -1; } close(fd); umask(old);//恢复默认的权限掩码 if ((fd = open("umask3.txt", O_RDWR | O_CREAT | O_TRUNC, 0666)) == -1) { perror("open"); return -1; } close(fd); return 0; }
14.修改权限
调用进程的有效用户ID必须与文件的拥有者用户ID匹配,或者是root用户,才能修改该文件的权限,且受权限掩码的影响。
chmod函数
#include <sys/stat.h>
int chmod(const char* pathname, mode_t mode);
成功返回0,失败返回-1。pathname - 文件路径
mode - 文件权限
fchmod函数
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
成功返回0,失败返回-1。fd - 文件描述符
mode - 文件权限
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
int fd = open("chmod.txt", O_RDWR | O_CREAT |
O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return -1;
}
/* SET USR GRP OTH
* 7 6 5 4
* 421 420 401 400
* ugt rw- r-x r--
* rwS r-s r-T
*/
if (fchmod(fd, 07654) == -1) {
perror("fchmod");
return -1;
}
close(fd);
return 0;
}
15.修改文件的拥有者和(或)拥有者组
如果调用进程的有效用户ID为root用户,则它可以任意修改任何文件的拥有者用户和组。如果调用进程的有效用户ID为普通用户,则它只能把自己名下文件的拥有者组改成自己隶属的其它组。
chown函数
#include <unistd.h>
int chown(const char* path, uid_t owner, gid_t group);
成功返回0,失败返回-1。path - 文件路径
owner - 拥有者用户ID,-1表示不修改,0表示root
group - 拥有者组ID,-1表示不修改,0表示root
fchown函数
#include <unistd.h>
int fchown(int fd, uid_t owner, gid_t group);
成功返回0,失败返回-1。fd - 文件描述符
owner - 拥有者用户ID,-1表示不修改,0表示root
group - 拥有者组ID,-1表示不修改,0表示root
lchown函数
#include <unistd.h>
int lchown(const char* path, uid_t owner, gid_t group); // 不跟踪符号链接
成功返回0,失败返回-1。path - 文件路径
owner - 拥有者用户ID,-1表示不修改,0表示root
group - 拥有者组ID,-1表示不修改,0表示root
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd = open("chown.txt", O_RDWR | O_CREAT |
O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return -1;
}
if (fchown(fd, 0, 0) == -1) {//0表示为root
perror("fchown");
return -1;
}
close(fd);
return 0;
}
16.修改文件大小
文件由大变小:截掉靠文件尾的部分。
文件由小变大:在文件尾之后增加0。
truncate函数
#include <unistd.h>
int truncate(const char* path, off_t length);
成功返回0,失败返回-1。path - 文件路径
length - 文件大小
ftruncate函数
#include <unistd.h>
int ftruncate(int fd, off_t length);
成功返回0,失败返回-1。fd - 文件描述符
length - 文件大小
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd = open("trunc.txt", O_RDWR |
O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return -1;
}
char const* text = "ABCDEFGH";
if (write(fd, text, strlen(text) * sizeof(
text[0])) == -1) {
perror("write");
return -1;
}
if (ftruncate(fd, 16) == -1) {
perror("ftruncate");
return -1;
}
close(fd);
if (truncate("trunc.txt", 4) == -1) {
perror("truncate");
return -1;
}
return 0;
}
//利用lseek覆盖读取数据,并把最后的数据利用truncate清理
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
typedef struct Student{
int age;
char sex;
float score;
}stu_t;
void main()
{
int fd = open("stduent.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if(fd == -1){
perror("open");
return;
}
stu_t stu[5] = {{20,'m',95},
{21,'f',96},
{22,'m',97},
{23,'f',98},
{24,'m',99}};
if(write(fd, (char*)stu, 5 * sizeof(stu_t)) == -1){
perror("write");
return;
}
off_t op = lseek(fd, 0, SEEK_SET);
if(op == -1){
perror("lseek");
return;
}
stu_t stu1[5];
for(int i = 0;i < 4;i++){
if(read(fd, &stu1[i], sizeof(stu_t)) == -1){
perror("read");
return;
}
if(i == 2){
if(lseek(fd, sizeof(stu_t), SEEK_CUR) == -1){
perror("lseek");
return;
}
}
}
if(ftruncate(fd, sizeof(stu_t)) == -1){
perror("ftruncate");
return;
}
for(int i = 0;i < 5;i++){
printf("age: %d\n"
"sex: %c\n"
"score: %f\n",stu1[i].age,stu1[i].sex,stu1[i].score);
}
return;
}
17.虚拟内存到磁盘文件的映射
mmap函数
建立虚拟内存到磁盘文件的映射
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void*类型的-1)前4个参数处理物理内存,后2个参数处理文件
start - 选择要映射区虚拟内存的起始地址,NULL表示自动选择(NULL安全) ,若自己指定也是按换页的大小4096的整数倍(向下取整)来做
length - 映射区的字节数,自动按页取整
prot - 访问权限,可取以下值:
PROT_READ - 可读
PROT_WRITE - 可写
PROT_EXEC - 可执行
PROT_NONE - 不可访问flags - 映射标志,取MAP_SHARED - 共享映射时,将虚拟内存映射到磁盘文件中 (此时没有内存壁垒,多个进程可以映射到同一磁盘文件,进而共享文件数据)
fd - 文件描述符
offset - 文件偏移量,自动按页对齐
munmap函数
解除虚拟内存到磁盘文件的映射(按换页大小的整数倍来操作)
#include <sys/mman.h>
int munmap(void* start, size_t length);
成功返回0,失败返回-1。start - 映射区的起始地址
length - 映射区的字节数
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(void)
{
char const* text = "Hello, World!";
size_t size = strlen(text) * sizeof(
text[0]);
int fd = open("mmap.txt", O_RDWR | O_CREAT |
O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return -1;
}
//扩大文件大小至需写入内存数据的大小
if (ftruncate(fd, size) == -1) {
perror("ftruncate");
return -1;
}
//建立虚拟内存到磁盘文件的映射
void* map = mmap(NULL, size, PROT_WRITE,
MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
return -1;
}
//内存拷贝,实质是在写文件
memcpy(map, text, size);
//解除映射
munmap(map, size);
close(fd);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main(void)
{
int fd = open("mmap.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return -1;
}
//利用元数据获取文件大小
struct stat st;
if (fstat(fd, &st) == -1) {
perror("fstat");
return -1;
}
//建立映射关系
void* map = mmap(NULL, st.st_size, PROT_READ,
MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
return -1;
}
//因为之前写入的字符串数据没有'\0'字符,所以这里要加上去保证输出的准确性
char text[st.st_size + 1];
memcpy(text, map, st.st_size);
text[st.st_size] = '\0';
printf("%s\n", text);
munmap(map, st.st_size);
close(fd);
return 0;
}
18.硬链接
硬链接就是文件路径,即由各级目录、分隔符(/)和文件名共同组成的字符串,与一个特定的i节点号所形成的对应关系。
ls -i
可查看i节点号和硬链接名可通过以下命令建立新路径硬链接,两路径同时标识一个文件
ln <源路径(已经存在的路径)> <目标路径(新建立的路径)>
link函数
根据一个已有的硬链接创建一个新的硬链接
#include <unistd.h>
int link(const char* oldpath, const char* newpath);
成功返回0,失败返回-1。oldpath - 已有的硬链接路径
newpath - 新的硬链接路径PS:oldpath必须存在,newpath中不能包含不存在目录。
//link函数例子 #include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc < 3) { fprintf(stderr, "用法:%s <原有路径> " "<新建路径>\n", argv[0]); return -1; } if (link(argv[1], argv[2]) == -1) { perror("link"); return -1; } return 0; }
unlink函数
删除硬链接
#include <unistd.h>
int unlink(const char* pathname);
成功返回0,失败返回-1。pathname - 文件路径(不能是目录)
从pathname所对应的目录文件中删除包含该文件的条目,同时将其对应的i节点中的硬链接数减一,若该硬链接数被减至0,则将该文件所占用的磁盘空间释放出来。
//unlink函数例子 #include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "用法:%s <路径>\n", argv[0]); return -1; } if (unlink(argv[1]) == -1) { perror("unlink"); return -1; } return 0; }
rename函数
修改硬链接
#include <unistd.h>
int rename(const char* oldpath, const char* newpath);
成功返回0,失败返回-1。oldpath - 原路径
newpath - 新路径例如:
rename("./a.txt", “./b.txt”); // 改名
rename(“a/1.txt”, “b/1.txt”); // 移动
rename(“a/1.txt”, “b/2.txt”); // 移动且改名//rename函数例子 #include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc < 3) { fprintf(stderr, "用法:%s <源路径> " "<目标路径>\n", argv[0]); return -1; } if (rename(argv[1], argv[2]) == -1) { perror("rename"); return -1; } return 0; }
remove函数
另一个版本的unlink,还可以删除空目录的硬链接:
#include <unistd.h>
int remove(const char* pathname);//remove函数例子 #include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "用法:%s <路径>\n", argv[0]); return -1; } if (remove(argv[1]) == -1) { perror("remove"); return -1; } return 0; }
19.软链接
软链接文件的本质就是保存着另一个文件或目录的路径的文件,因此链接起来的两文件的i节点号是不同的。
根据一个已有的硬链接创建一个符号链接,即为软链接。可通过以下命令建立软链接:
ln -s <源路径(已经存在的路径)> <目标路径(新建立的路径)>
symlink函数
#include <unistd.h>
int symlink(const char* oldpath, const char* newpath);
成功返回0,失败返回-1。oldpath - 原有路径,可以是文件,也可以是目录,甚至可以不存在(因为软链接的底层逻辑就是新建一个文件以字符串的形式将另一文件的路径写入)
newpath - 新建路径,不能包含不存在目录
readlink函数
读取软链接文件本身的内容
#include <unistd.h>
ssize_t readlink(const char* path, char* buf, size_t size);
成功返回拷入buf的符号链接文件内容的字节数,失败返回-1。path - 软链接文件路径
buf - 缓冲区
size - 缓冲区大小
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
int main(int argc, char* argv[])
{
if (argc < 3) {
fprintf(stderr, "用法:%s <文件> "
"<软链接>\n", argv[0]);
return -1;
}
if (symlink(argv[1], argv[2]) == -1) {
perror("symlink");
return -1;
}
char slink[PATH_MAX + 1] = {};//PATH_MAX为limits.h库的宏,为最大路径长,+1位存放'\0'字符
if (readlink(argv[2], slink, sizeof(slink) -
sizeof(slink[0])) == -1) {
perror("readlink");
return -1;
}
printf("%s -> %s\n", argv[2], slink);
return 0;
}
20.目录
mkdir函数
创建一个空目录
#include <sys/stat.h>
int mkdir(const char* pathname, mode_t model);
成功返回0,失败返回-1。pathname - 目录路径
mode - 访问权限,目录的执行权限(x)表示可以进入
rmdir函数
删除一个空目录
int rmdir(const char* pathname);
成功返回0,失败返回-1。pathname - 目录路径
remove函数 = unlink函数 + rmdir函数
getcwd函数
获取当前工作目录
char* getcwd(char* buf, size_t size);
成功返回工作目录字符串指针,即buf,失败返回NULL。buf - 缓冲区
size - 缓冲区大小 自动追加结尾空字符。当前工作目录作为进程的属性之一,也是系统内核进程表项的一部分。
chdir函数
改变当前工作目录
int chdir(const char* path);
成功返回0,失败返回-1。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
int main(void) {
char cwd[PATH_MAX + 1];
if (!getcwd(cwd, sizeof(cwd))) {
perror("getcwd");
return -1;
}
printf("当前工作目录:%s\n", cwd);
if (mkdir("work", 0755) == -1) {
perror("mkdir");
return -1;
}
if (chdir("work") == -1) {
perror("chdir");
return -1;
}
if (!getcwd(cwd, sizeof(cwd))) {
perror("getcwd");
return -1;
}
printf("当前工作目录:%s\n", cwd);
if (chdir("..") == -1) {
perror("chdir");
return -1;
}
if (!getcwd(cwd, sizeof(cwd))) {
perror("getcwd");
return -1;
}
printf("当前工作目录:%s\n", cwd);
if (rmdir("work") == -1) {//只能删空目录
perror("rmdir");
return -1;
}
return 0;
}
opendir函数
打开目录
#include <dirent.h>
DIR* opendir(const char* name);
成功返回目录流指针,失败返回NULL。
readdir函数
读取目录
#include <dirent.h>
struct dirent* readdir(DIR* dirp);
成功返回目录条目指针,读完(不设置errno)或失败(设置errno)返回NULL。dirp - 目录流指针
主要的目录条目 struct dirent { ino_t d_ino; // 节点号 off_t d_off; // 下一条位置(索引),用来查找信息,是线性的,但效率不高,现在用哈希表效率更高 unsigned short d_reclen; // 记录长度 unsigned char d_type; // 文件类型 char d_name[]; // 文件名 };
closedir函数
关闭目录流
#include <dirent.h>
int closedir(DIR* dirp);
成功返回0,失败返回-1。dirp - 目录流指针
#include <stdio.h>
#include <dirent.h>
#include <errno.h>
int main(int argc, char* argv[])
{
if (argc < 2) {
fprintf(stderr, "用法:%s <目录>\n",
argv[0]);
return -1;
}
DIR* dp = opendir(argv[1]);
if (!dp) {
perror("opendir");
return -1;
}
errno = 0;//更新下错误码,错误码不会自己维护更新
struct dirent* de;
for (de = readdir(dp); de; de = readdir(dp)){
switch (de->d_type) {
case DT_DIR:
printf(" 目录:");
break;
case DT_REG:
printf(" 普通文件:");
break;
case DT_LNK:
printf(" 软链接:");
break;
case DT_BLK:
printf(" 块设备:");
break;
case DT_CHR:
printf(" 字符设备:");
break;
case DT_SOCK:
printf("本地套接字:");
break;
case DT_FIFO:
printf(" 有名管道:");
break;
default:
printf(" 未知:");
break;
}
printf("%8lu %s\n",
de->d_ino, de->d_name);
}
if (errno) {
perror("readdir");
return -1;
}
closedir(dp);
return 0;
}