目录
我们要进行文件操作,前提是我们的程序跑起来了,文件打开和关闭,是CPU在执行我们的代码 。
打开文件本质是进程打开文件,文件没有打开的时候在磁盘,一个进程可以打开很多文件。
很多情况下,OS内部,一定存在大量被打开的文件
文件=属性+内容
管理方式:先描述,再组织 类似PCB的结构体
理解文件
文件 -> 磁盘 -> 外设 -> 硬件 -> 向文件中写入,本质是向硬件中写入 -> 用户没有权利直接写入 -> OS是硬件的管理者 -> 通过OS写入 -> OS给我们提供系统调用
访问文件我们可以用系统调用,也可以用封装好的函数fopen/fwrite/fread/fprintf/scanf/printf/cin/cout?->我们用的C/C++/...都是对系统调用接口的封装!
使用和认识系统调用的文件操作
打开文件系统接口函数
- pathname:文件路径或者文件名(当前路径下)
- flags:位图传递
- 32个比特位,用比特位进行标志位的传递
- mode:文件权限
- 实际文件权限 mode&~umask
- 返回值:(int类型变量)fd
O_WRONLY:写方式打开
O_CREAT:不存在就当前路径创建
O_TRUNC:存在就清空
O_APPEND:存在就追加O_WRONLY 和 O_CREAT :宏 内部是一个传递位图标记位的函数
#define ONE 1 // 1 0000 0001 #define TWO (1<<1) // 2 0000 0010 #define THREE (1<<2) // 4 0000 0100 #define FOUR (1<<3) // 8 0000 1000 void print(int flag) { if(flag&ONE) printf("one\n"); //替换成其他功能 if(flag&TWO) printf("two\n"); if(flag&THREE) printf("three\n"); if(flag&FOUR) printf("four\n"); } int main() { print(ONE); printf("\n"); print(TWO); printf("\n"); print(ONE|TWO); printf("\n"); print(ONE|TWO|THREE); printf("\n"); print(ONE|FOUR); printf("\n"); print(ONE|TWO|THREE|FOUR); printf("\n"); return 0; }
关闭文件系统接口函数
向文件中写入系统接口函数
向文件中读出数据系统接口函数
fd是什么
C语言封装操作函数中的FILE是一个结构体,里面包含fd的数据
文件描述符fd的本质是:内核的进程:文件映射关系的数组的下标
系统一启动,默认打开三个文件,0:标准输入,1:标准输出,2:标准错误
我们调用C语言封装的函数底层就调用系统调用接口了
open在干什么呢
1.创建file
2.开辟文件缓冲区的空间,加载文件数据
3.查进程的文件描述符表
4.file地址填入对应的表下标中
5.返回下标
无论读写,都必须在合适的时候,让OS把文件的内容读到文件缓冲区中
所有的C语言上面的文件操作函数,本质底层都是对系统调用的封装
stat系统接口
这三个函数是输出型系统接口函数,输出一个结构体,这结构体是获取某个文件属性的结构体
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
const char *filename = "log.txt";
int main()
{
struct stat st;
int n = stat(filename, &st);
if(n<0) return 1;
printf("file size: %lu\n", st.st_size);
int fd = open(filename, O_RDONLY);
if(fd < 0)
{
perror("open");
return 2;
}
printf("fd: %d\n", fd);
char *file_buffer = (char*)malloc(st.st_size + 1);
n = read(fd, file_buffer, st.st_size);
if(n > 0)
{
file_buffer[n] = '\0';
printf("%s", file_buffer);
}
free(file_buffer);
close(fd);
return 0;
}
文件描述符的分配规则
输出发现是 fd: 3
关闭 0 或者 2 ,在看
发现是结果是: fd: 0 或者 fd 2
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
重定向
那如果关闭1呢?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。 每用C语言打开一个文件,就会形成一个struct FILE结构体存储相关信息和语言缓冲区
int dup2(int oldfd,int newfd)
本质是文件描述符下标所对应内容的拷贝
oldfd拷贝给newfd这里将下标1对应的显示器改为文件,也就向文件追加打印“hello world”
缓冲区的理解
缓冲区的理解缓冲区刷新策略
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> const char *filename = "log.txt"; int main() { // C printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); // system call const char *msg = "hello write\n"; write(1, msg, strlen(msg)); fork(); //??? return 0; }
这里创建了父子进程,C语言写入函数先写入用户缓冲区,系统接口写入函数直接写入文件内核缓存。最后父子进程结束,刷新两次用户缓冲区,将数据导入内核缓存两次从而写进文件
myshell功能的完善 (追加重定向功能)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
#define SkipSpace(cmd, pos) do{\
while(1){\
if(isspace(cmd[pos]))\
pos++;\
else break;\
}\
}while(0)
// "ls -a -l -n > myfile.txt"
#define None_Redir 0
#define In_Redir 1
#define Out_Redir 2
#define App_Redir 3
int redir_type = None_Redir;
char *filename = NULL;
// 为了方便,我就直接定义了
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
void Die()
{
exit(1);
}
const char *GetHome()
{
const char *home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
const char *GetUserName()
{
const char *name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
// 临时
const char *GetCwd()
{
const char *cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
// commandline : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
void SplitCommand(char command[], size_t n)
{
(void)n;
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0] = strtok(command, SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//重定向设置
if(filename != NULL){
if(redir_type == In_Redir)
{
int fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == Out_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == App_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{}
}
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp, sizeof(temp));
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd); // OK
}
int CheckBuildin()
{
int yes = 0;
const char *enter_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
return yes;
}
void CheckRedir(char cmd[])
{
// > >> <
// "ls -a -l -n > myfile.txt"
int pos = 0;
int end = strlen(cmd);
while(pos < end)
{
if(cmd[pos] == '>')
{
if(cmd[pos+1] == '>')
{
cmd[pos++] = 0;
pos++;
redir_type = App_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
cmd[pos++] = 0;
redir_type = Out_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
}
else if(cmd[pos] == '<')
{
cmd[pos++] = 0;
redir_type = In_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
pos++;
}
}
}
int main()
{
int quit = 0;
while(!quit)
{
// 0. 重置
redir_type = None_Redir;
filename = NULL;
// 1. 我们需要自己输出一个命令行
MakeCommandLineAndPrint();
// 2. 获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1;
// 2.1 checkredir
CheckRedir(usercommand);
// 2.2 debug
// printf("cmd: %s\n", usercommand);
// printf("redir: %d\n", redir_type);
// printf("filename: %s\n", filename);
//
// 3. 命令行字符串分割.
SplitCommand(usercommand, sizeof(usercommand));
// 4. 检测命令是否是内建命令
n = CheckBuildin();
if(n) continue;
// 5. 执行命令
ExecuteCommand();
}
return 0;
}
简单库的模拟与封装
mystdio.h
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LINE_SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4
struct _myFILE
{
unsigned int flags;
int fileno;
// 缓冲区
char cache[LINE_SIZE];
int cap;
int pos; // 下次写入的位置
};
typedef struct _myFILE myFILE;
myFILE* my_fopen(const char *path, const char *flag);
void my_fflush(myFILE *fp);
ssize_t my_fwrite(myFILE *fp, const char *data, int len);
void my_fclose(myFILE *fp);
mystdio.c
#include "mystdio.h"
myFILE* my_fopen(const char *path, const char *flag)
{
int flag1 = 0;
int iscreate = 0;
mode_t mode = 0666;
if(strcmp(flag, "r") == 0)
{
flag1 = (O_RDONLY);
}
else if(strcmp(flag, "w") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
iscreate = 1;
}
else if(strcmp(flag, "a") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_APPEND);
iscreate = 1;
}
else
{}
int fd = 0;
if(iscreate)
fd = open(path, flag1, mode);
else
fd = open(path, flag1);
if(fd < 0) return NULL;
myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp) return NULL;
fp->fileno = fd;
fp->flags = FLUSH_LINE;
fp->cap = LINE_SIZE;
fp->pos = 0;
return fp;
}
void my_fflush(myFILE *fp)
{
write(fp->fileno, fp->cache, fp->pos);
fp->pos = 0;
}
ssize_t my_fwrite(myFILE *fp, const char *data, int len)
{
// 写入操作本质是拷贝, 如果条件允许,就刷新,否则不做刷新
memcpy(fp->cache+fp->pos, data, len); //肯定要考虑越界, 自动扩容
fp->pos += len;
if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n')
{
my_fflush(fp);
}
return len;
}
void my_fclose(myFILE *fp)
{
my_fflush(fp);
close(fp->fileno);
free(fp);
}
stderr(fd=2)是什么
stdout和stderr都连接显示器文件,是为了区分正确和错误的消息分成了两个结构体
正确消息是通过stdout打印显示器,错误消息是通过stderr打印显示器
/a.out > log.txt 这样默认重定向fd=1结构体的指向,指向log.txt文件
./a.out 1>all.log 2>err.log 这样可以重定向指向两个文件实现分类
./a.out 1>all.log 2>&1 正确和错误消息写在一个文件
printf(); cout<<; 都是向1打印
perror(); cerr<<; 都是向2打印
理解文件系统
文件=内容+属性(也是数据)
文件的磁盘存储,本质是:文件的内容+文件的属性数据
Linux文件系统特定:文件内容和文件属性分开存储
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。政府管理各区的例子
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了(不是每个分组都有)
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没 有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
inode节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容Linux中文件的属性是一个大小固定的结构体
一个正常文件,一个inode属性集合
struct inode { int size; mode_t mode; int creater; int time; ...... int datablocks[N]; }
inode内部不包含文件名,内核层面每一个文件都要有inode number,我们通过这个inode号标识一个文件
我们寻找文件的时候必须先得到文件的inode号
(inode编号是以分区为单位的,datablock也是如此)文件较大时,可通过拓展映射存储到磁盘
inode编号如何拿到
目录=文件属性+文件内容
目录的文件内容就是文件名和inode编号的映射关系
我们使用文件名的时候,就自动索引到文件名对应的inode编号找到指定的文件->文件所在的目录->打开->根据文件名索引inode->目标文件的inode
目录也是文件,目录名索引它的inode
逆向的路径解析
/home/whb/111/code/lesson/dir
Linux会缓存路径结构
目录通常是谁提供的,进程早就提供了
内核文件系统提前写入并组织好,然后提供
软硬链接
1.软链接是一个独立的文件,因为有独立的inode number
软链接的内容->目标文件所对应的路径字符串,类似于windows的快捷方式
用rm或unlink删除源文件后就会出现报错
2.硬链接不是一个独立的文件,因为没有独立的inode number,用的是目标文件inode number3.属性中有一列硬链接数)
结论:1.构建Linux的路径结构,让我们可以使用 .和..来进行路径定位
2.一般用硬链接来做文件备份
动态库和静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
Linux .so(动态库) .a(静态库) 静态库
将.o文件进行打包就是静态库
静态库和头文件封装安装到系统里面然后调用
如果静态库和头文件封装后没有安装在系统目录中,需要这样调用
main.c文件内部头文件路径可以不写,但必须包含的时候包含头文件路径
动态库
库名规则:libxxx.so
gcc在不使用static选项的时候,默认使用动态库
编译正常生成.exe文件,但执行调用动态库时,出现执行失败
原因:没有找到动态库。
方法:
a.
b.
c.
总结:
动态库加载-----可执行程序和地址空间
我们的可执行程序,编译成功,没有加载运行,二进制代码中包含了地址
ELF格式的可执行程序,二进制是有自己的固定格式的
(elf可执行程序的头部,可执行程序的属性)
这些地址其实就是虚拟地址(逻辑地址)
ELF+加载器:各个区域的起始和结束地址,main函数的入口地址