CS110操作系统原理学习笔记--文件系统(一)
本篇笔记主要涵盖CS110第1到第4节课关于文件系统的部分。
https://www.youtube.com/watch?v=_LFGjZ0Sc6I&list=PLai-xIlqf4JmTNR9aPCwIAOySs1GOm8sQ
Lecture 1
- umask, open, read和write系统调用
- touch实现
- copy实现
1.umask和open系统调用
1.1.umask
https://man7.org/linux/man-pages/man2/umask.2.html
umask是一个根据掩码判断文件权限的命令,也可以用作系统调用来设置新文件创建时候的权限。
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask); // mode_t -> Uint32
mode_t old_mask = umask(0); // umask设为0,返回的是原先的umask
第一个是d则表示是文件夹
drwx------ 2 vivek vivek 4096 2011-03-04 02:05 dir1
-rw------- 1 vivek vivek 0 2011-03-04 02:05 file
1.2.open
https://man7.org/linux/man-pages/man2/open.2.html
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // mode控制权限
flags用bitset控制, 必须包含下面三个中的一个读写申明
- O_RDONLY // 只读
- O_WRONLY // 只写
- O_RDWR // 可读可写
处理方法(用||做or的bitset)
- O_CREAT 如果文件不存在,新建一个
- O_EXCL 如果文件存在,报错(写的时候才用)
(写入方法) - O_APPEND 用O_APPEND就是在文件尾继续写
- O_TRUNC 抹除文件内容,再写
可处理方法和写入方法可一起用 O_CREAT|O_WRONLY|O_TRUNC, O_CREAT|O_WRONLY| O_EXCL
1.3.read
read 会尝试从文件描述符中读取count大小的bytes,并放到buffer中(starting at buf)。返回值为成功读取的bytes,如果0表示读完了,-1表示异常(文件位置改变...)。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
1.4.write
从buf指向的位置开始写入count个bytes到文件描述符fd中。 返回值为成功写入的bytes的数量,0表示没东西可写,-1表示异常。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
2.简单的touch实现
errno是c的一个全局变量用来存储程序的异常, errno -l查询错误码对应的异常。
#include <stdio.h>
#include <fcntl.h> // for open
#include <unistd.h> // for read, open, write
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
const char *kFilename = "my_file";
const int kFileExistsErr = 17;
int main() {
umask(0); // 重置默认umask再修改umask
int file_descriptor = open(kFilename, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (file_descriptor == -1) {
printf("There was a problem creating '%s'!\n", kFilename);
if (errno == kFileExistsErr) {
printf("The file already exist!\n");
} else {
printf("Unknown errono: %d\n", errno);
}
return -1;
}
close(file_descriptor);
return 0;
}
权限也为0644
3.简单的cp实现
The read system call will block until the requested number of bytes have been read. If the return value is 0, there are no more bytes to read (e.g., the file has reached the end, or been closed).
If write returns a value less than count, it means that the system couldn't write all the bytes at once. This is why the while loop is necessary, and the reason for keeping track of bytesWritten and bytesRead.
You should close files when you are done using them, although they will get closed by the OS when your program ends. We will use valgrind to check if your files are being closed.
#include <stdio.h>
#include <fcntl.h> // for open
#include <unistd.h> // for read, close, write
#include <stdbool.h> // bool
#include <errno.h>
static const int kWrongArgumentCount = 1;
static const int kSourceFileNonExistent = 2;
static const int kDestinationFileOpenFailure = 4;
static const int kReadFailure = 8;
static const int kWriteFailure = 16;
static const int kDefaultPermissions= 0644;
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "%s <source-file> <destination-file>. \n", argv[0]);
return kWrongArgumentCount;
}
int fdin = open(argv[1], O_RDONLY);
// 只写,创建+覆盖
int fdout = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, 0644);
char buffer[1024];
while (true) { // 保证文件被读完
ssize_t bytesRead = read(fdin, buffer, sizeof(buffer));
if (bytesRead == 0) break; // 读完了
size_t bytesWritten = 0; // 保证readBuffer的写完
while (bytesWritten < bytesRead) {
bytesWritten += write(fdout, buffer + bytesWritten, bytesRead - bytesWritten);
}
}
close(fdin);
close(fdout);
return 0;
}
open能对很多底层的东西进行操作,在其他情况下,用FILE和iostream就好了
read会block到把所有东西给读完
Lecture 2
- 文件操作方式的总结
- tee的实现
- stat和lstat
- ls的实现
文件操作方式总结
open和write太low-level了,编程中多用iostream和FILE*来操作文件
- FILE指针 fopen
FILE * fopen ( const char * filename, const char * mode );
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
fopen和open对文件的操作方式有自己的flag需要自行查阅
mode(http://www.cplusplus.com/reference/cstdio/fopen/)
- iostream类
ifstream fin(filename); // 读
// 两种读的方式
fin.read (buffer,length);
getline (fin,line);
ofstream fout(filename); // 写
fout << "sth";
fstream 的文件操作方式也有自己的flag,比如ios::ate
https://www.cs.uic.edu/~jbell/CourseNotes/CPlus/FileIO.html
2.tee
tee能把标准输入转到文件上然后再拷贝一份到标准输出
#include <fcntl.h> // for open
#include <unistd.h> // for read, close, write
#include <stdbool.h> // bool
#include <errno.h>
#define DEFAULT_FLAGS (O_WRONLY | O_CREAT | O_TRUNC)
#define DEFAULT_PERMISSIONS 0644
int main(int argc, char *argv[]) {
int fds[argc];
fds[0] = STDOUT_FILENO;
for (size_t i = 1; i < argc; i++)
fds[i] = open(argv[i], DEFAULT_FLAGS , DEFAULT_PERMISSIONS );
char buffer[2048];
while (true) {
ssize_t numRead = read(STDIN_FILENO, buffer, sizeof(buffer));
if (numRead == 0) break;
for (size_t i = 0; i < argc; i++) writeall(fds[i], buffer, numRead);
}
for (size_t i = 1; i < argc; i++) close(fds[i]);
return 0;
}
static void writeall(int fd, const char buffer[], size_t len) {
size_t numWritten = 0;
while (numWritten < len) {
numWritten += write(fd, buffer + numWritten, len - numWritten);
}
}
stat和lstat
查看文件属性。当文件是一个link时候,stat放回link指向的文件,lstat返回link本身
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);
stat结构体, 文件信息大部分都在st_mode中,用定义好的宏可以查看相关信息,比如int S_ISDIR (mode_t m)。https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B 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 */
};
ls的实现
实现ls需要能得到文件的umask和mode_st关于文件的信息(S_ISDIR,S_ISREG,S_ISLNK), 文件夹的操作opendir/closedir。
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *);
struct dirent *readdir(DIR *);
DIR结构体(directory stream, 使用readdir遍历目录下所有的entry),文件夹中文件的信息(struct dirents), 遍历结束放回NULL
// dirent
// ino_t d_ino file serial number
// char d_name[] name of entry
dirent结构体 (https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html)
除了名字和inode还有很多
static void listMatches(char path[], size_t length, const char *name) {
DIR *dir = opendir(path);
if (dir == NULL) return; // it's a directory, but permission to open was denied
strcpy(path + length++, "/");
while (true) {
struct dirent *de = readdir(dir);
if (de == NULL) break; // we've iterated over every directory entry, so stop looping
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
if (length + strlen(de->d_name) > kMaxPath) continue;
strcpy(path + length, de->d_name);
struct stat st;
lstat(path, &st);
if (S_ISREG(st.st_mode)) {
if (strcmp(de->d_name, name) == 0) printf("%s\n", path);
} else if (S_ISDIR(st.st_mode)) {
listMatches(path, length + strlen(de->d_name), name);
}
}
closedir(dir);
}
int main(int argc, char *argv[]) {
assert(argc == 3);
const char *directory = argv[1];
struct stat st;
lstat(directory, &st);
assert(S_ISDIR(st.st_mode));
size_t length = strlen(directory);
if (length > kMaxPath) return 0; // assume kMaxPath is some #define
const char *pattern = argv[2];
char path[kMaxPath + 1];
strcpy(path, directory); // buffer overflow impossible
listMatches(path, length, pattern);
return 0;
}
http://web.stanford.edu/class/cs110/examples/filesystems/list.c
https://www.gnu.org/software/libc/manual/html_node/Opening-a-Directory.html#Opening-a-Directory
readdir实现
// DIR指针记录了文件block的offset,每次readdir也会更新offset,所以返回的就是下一个dir entry. offset等于dirent block的大小会结束并重置offset
/* Read a directory entry from DIRP. */
DIRENT_TYPE *
__READDIR (DIR *dirp)
{
DIRENT_TYPE *dp;
int saved_errno = errno;
do
{
size_t reclen;
if (dirp->offset >= dirp->size) // dir 记录的dir entries已被遍历完了, offset == size
{
/* We've emptied out our buffer. Refill it. */
size_t maxread;
ssize_t bytes;
/* Fixed-size struct; must read one at a time (see below). */
maxread = sizeof *dp;
bytes = __GETDENTS (dirp->fd, dirp->data, maxread);
/*省略if(bytes <= 0) ....*/
dirp->size = (size_t) bytes;
/* Reset the offset into the buffer. */
dirp->offset = 0;
}
dp = (DIRENT_TYPE *) &dirp->data[dirp->offset]; // 获得dir entry
reclen = dp->d_reclen;
dirp->offset += reclen; // offset + entry大小
dirp->filepos = dp->d_off;
} while (dp->d_ino == 0); \* Skip deleted files. inode为0->比如文件删除了会设inode为0 *\
return dp;
}
获得文件umask
static inline void updatePermissionsBit(bool flag, char permissions[],
size_t column, char ch) {
if (flag) permissions[column] = ch;
}
static const size_t kNumPermissionColumns = 10;
static const char kPermissionChars[] = {'r', 'w', 'x'};
static const size_t kNumPermissionChars = sizeof(kPermissionChars);
static const mode_t kPermissionFlags[] = {
S_IRUSR, S_IWUSR, S_IXUSR, // user flags
S_IRGRP, S_IWGRP, S_IXGRP, // group flags
S_IROTH, S_IWOTH, S_IXOTH // everyone (other) flags
};
static const size_t kNumPermissionFlags =
sizeof(kPermissionFlags)/sizeof(kPermissionFlags[0]); // 9
static void listPermissions(mode_t mode) {
char permissions[kNumPermissionColumns + 1];
memset(permissions, '-', sizeof(permissions));
permissions[kNumPermissionColumns] = '\0';
updatePermissionsBit(S_ISDIR(mode), permissions, 0, 'd');
updatePermissionsBit(S_ISLNK(mode), permissions, 0, 'l');
for (size_t i = 0; i < kNumPermissionFlags; i++) {
updatePermissionsBit(mode & kPermissionFlags[i], permissions, i + 1,
kPermissionChars[i % kNumPermissionChars]); // 从1开始,0记录的是文件属性,比如dir或link
}
printf("%s ", permissions);
}
LECTURE3 filesystem design
- 磁盘与文件系统
- inode
磁盘与文件系统
磁盘与扇区
RAM上的信息是由byte来编排的,如果写入1bit,需要读1byte来获得那一bit的数据(word其实更准确)。这和在磁盘上的文件非常相似,磁盘可以看成是由分区进行编排的,因此我们必须对一个分区为单位进行读和写。所谓的驱动实质上是一个硬件的api。 在文件系统中,多用block而不是sector来表示一个区块,一个block可以为多个sector
硬件不单存储文件数据,还需要不少metadata来支持文件系统
- boot block: 包含开机用的bootstrap程序
- superblock: 记录文件系统的configuration,用于比如重新挂载文件系统
- inode table: 储存文件的metadata(inode), 占了10%的磁盘空间
- File contents: 储存文件内容,一个block最多存一个文件,一个文件如果太大会存到多个block。
inode储存了文件的信息,如文件所有者,文件权限,创建时间,文件类型和大小等。如图的inode 2(绿色),文件大小是1028B(假设block大小是512B), inode 2对应的文件内容就要存在3个block中。文件在硬件上的存储不需要连续(文件系统其实会提供虚拟化来提高locality)
inode & file
struct inode {
uint16_t i_mode; // bit vector of file type and permissions
uint8_t i_nlink; // number of references to file, 几个link
uint8_t i_uid; // owner
uint8_t i_gid; // group of owner
uint8_t i_size0; // most significant byte of size
uint16_t i_size1; // lower two bytes of size (size is encoded in a three-byte number)
uint16_t i_addr[8]; // device addresses constituting file inode指向的block
uint16_t i_atime[2]; // access time
uint16_t i_mtime[2]; // modify time
};
在文件夹类型的文件内容中,存放着文件的名字(14bytes)和inode(2bytes)。在检索一个文件比如/usr/example.txt时,先找/的inode(通常根的inode是1),再获得usr的inode然后就能找到example.txt的位置了(通过文件的sector能映射到文件内容)。
根据路径找到文件的步骤:
硬连接和软连接
硬连接相当于一个指向同个inode的文件, 创建硬连接会让区块的引用+1(i_nlink), 所以删掉源文件只要硬连接的文件在,区块就不会清除;而软连接像一个别名,软连接的inode保存了一个源文件的绝对路径,当访问软连接时候自动替换成这个绝对路径。
ln file hard
ln -s file soft
https://www.jianshu.com/p/dde6a01c4094
大文件的存放
inode中存放文件区块的数组只有8的大小,为了存放更大的文件,使用了indirect addressing的方法,及其中一个文件区存放的inode是用来存放inode的。。doubly indirect下最高可以达到单个文件40多G。
LECTURE 4 Filesystems Data Structures
- PCB和file desciptor
Linux对每个active的进程都维护着一个process control block(PCB),PCBs 存放的地方叫做process table。PCB还保存着描述符表(decriptor table)。
描述符表里存放着指向open file table entry的指针, 这里的entry记录着打开的模式(只读只写,etc.)和指针在文件的位置等(这就是fd不同与inode的地方)。根据fd找到描述符表对应的open file entry。
在open file中保存着当前的指针(cursor),打开方式,refcount还有vnode指针。vnode像是文件的cache,保存着文件的inode等信息,所以不需要再从根目录开始找这个文件的信息(比如inode)
File Decriptors -> File Table -> vnode Table
文件描述符和open file table entry是不同的东西。文件描述符表中存着open file的指针,文件描述符是文件描述符表的index,所以能由文件描述符找到指定的open file,多个描述符也可以指向同个open file。
./main 1> log.txt 2> log.txt // 打开两次文件,两个file table entries
./main 1> log.txt 2>&1 // 打开1次文件,两个描述符指向同个file table entry
// 2> redirects stderr to an (unspecified) file, appending &1 redirects stderr to stdout.
// gcc main.c -o main
const char* error = "This is my error message.\n It's terrible.\n";
const char* msg = "This is my coolest msg. You are\n";
int main() {
write(2, error, strlen(error));
write(1, msg, strlen(msg));
}
inode和fd的区别
- inode主要是帮助系统在磁盘找到文件内容
- fd主要负责读写
inode 不记录文件当前指针的位置(cursor)和打开的模式(可读可写), 而可以通过fd找到file table的对应entry进行追踪
https://www.usna.edu/Users/cs/wcbrown/courses/IC221/classes/L09/Class.html