网站建设优化是什么鬼?,网站建设哪里学,游戏开发是什么,室内设计联盟官网入口网页Linux 1.理解文件2.C文件接口1.打开 写文件2.读文件 简单实现cat命令3.输出信息到显示器的方式4.stdin、stdout、stderr5.打开文件的方式 3.系统接口 IO1.传递标志位2.open、close3.write、read 4.文件描述符1.是什么#xff1f;2.分配规则3.重定向原理4.通过dup2系统调用重… Linux 1.理解文件2.C文件接口1.打开 写文件2.读文件 简单实现cat命令3.输出信息到显示器的方式4.stdin、stdout、stderr5.打开文件的方式 3.系统接口 IO1.传递标志位2.open、close3.write、read 4.文件描述符1.是什么2.分配规则3.重定向原理4.通过dup2系统调用重定向5.标准错误重定向6.自定义shell添加重定向功能 5.理解一切皆文件6.缓冲区1.什么是缓冲区2.FILE3.缓冲类型4.为什么要引入缓冲区机制5.设计文件libc库 1.理解文件
狭义理解 文件在磁盘中。磁盘是永久性存储介质因此文件在磁盘上的存储是永久性的。磁盘是外设输出设备/输入设备磁盘上的文件本质是对文件的所有操作都是对外设的输入和输出简称 “IO”。 广义理解 Linux 下一切皆文件键盘、显示器、网卡、磁盘…… 文件操作 对于 0KB 的空文件是占用磁盘空间的。文件 文件属性元数据 文件内容。所有的文件操作本质是文件内容操作和文件属性的操作。 系统角度 访问文件的前提是先打开文件谁打开文件呢答案是 “进程打开文件”。对文件的操作本质就是 “进程对文件的操作”。磁盘的管理者是操作系统访问文件本质就是 “访问磁盘”只有操作系统才能访问磁盘文件。文件的读写本质不是通过C语言/C的库函数来操作的这些库函数只是为用户提供方便而是通过文件相关的系统调用接口来实现的。fopen 和 fclose 封装了操作系统对文件的系统调用。操作系统通过 “先描述再组织” 的方式对文件进行管理。在操作系统内部对被打开的文件创建 struct 结构体包含被打开文件的相关属性这些结构体通过链表的形式组织起来对被文件的管理转化成对链表的 “增删查改”。 2.C文件接口
1.打开 写文件
#includestdio.h
#includestring.hint main()
{FILE* fp fopen(log.txt, w);if(fp NULL){perror(fopen);return 1; }int cnt 1;const char* msg Hello Linux; while(cnt 10) { char buffer[1024]; snprintf(buffer, sizeof(buffer), %s%d\n, msg, cnt);fwrite(buffer, strlen(buffer), 1, fp);} fclose(fp);return 0;
}2.读文件 简单实现cat命令
#includestdio.h
#includestring.hint main(int argc, char* argv[])
{if(argc ! 2){printf(Usage%s filename\n, argv[0]); return 1;}FILE* fp fopen(argv[1], r);if(fp NULL){perror(fopen);return 2;}while(1){char buffer[128];memset(buffer, 0, sizeof(buffer));int n fread(buffer, sizeof(buffer) - 1, 1, fp);if(n 0)printf(%s, buffer);if(feof(fp)) //当到了文件的结尾退出循环break;} fclose(fp);return 0;
}3.输出信息到显示器的方式
Linux 中一切皆文件显示七也是一种文件
#includestdio.h
#includestring.h int main()
{ printf(Hello printf\n); fprintf(stdout, Hello fprintf\n);const char* msg Hello fwrite\n;fwrite(msg, strlen(msg), 1, stdout);return 0;
} 4.stdin、stdout、stderr C 默认会打开三个输入输出流分别是stdin、stdout、stderr仔细观察发现这三个流的类型都是FILE* fopen返回值类型是文件指针。 #include stdio.hextern FILE *stdin; //标准输入键盘文件
extern FILE *stdout; //标准输出显示器文件
extern FILE *stderr; //标准错误显示器文件5.打开文件的方式 3.系统接口 IO 打开文件的方式不仅仅是fopenifstream等语言层的方案其实系统才是打开文件最底层的方案。不过在学习系统文件 IO 之前先要了解下如何给函数传递标志位该方法在系统文件 IO 接口中会使用到 1.传递标志位 当存在多个标记位时一般的做法是传递多个参数用起来非常麻烦。操作系统采用 “位图” 来 “传递标志位” 的方式32个比特位每一个比特位的 0/1 代表是否被设置。 传递标志位的代码案例
#includestdio.h#define ONE_FLAG 10 //0000 0000 ... 0000 0001
#define TWO_FLAG 11 //0000 0000 ... 0000 0010
#define THREE_FLAG 12 //0000 0000 ... 0000 0100
#define FOUR_FLAG 13 //0000 0000 ... 0000 1000void fun(int flags)
{if(flags ONE_FLAG) printf(one\n);if(flags TWO_FLAG) printf(two\n);if(flags THREE_FLAG) printf(three\n);if(flags FOUR_FLAG) printf(four\n);
}int main()
{fun(ONE_FLAG); printf(\n);fun(ONE_FLAG | TWO_FLAG); printf(\n);fun(ONE_FLAG | TWO_FLAG | THREE_FLAG); printf(\n);fun(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG); printf(\n);return 0;
}2.open、close
#include sys/types.h
#include sys/stat.h
#include fcntl.hint open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);pathname要打开或创建的目标文件。flags文件描述符。打开文件时可以传入多个参数选项用下面的一个或者多个常量进行 “按位或” 运算构成 flags。参数O_RDONLY只读打开、O_WRONLY只写打开、O_RDWR读写打开。这三个常量必须指定一个且只能指定一个。O_CREAT若文件不存在则创建它。O_APPEND追加写。mode当文件不存在时以O_WRONLY打开文件指明创建新文件的访问权限。返回值成功时返回新打开的文件描述符。失败时返回-1 #include unistd.hint close(int fd);fd文件描述符open 函数的返回值。返回值成功时返回0失败时返回-1 #include sys/types.h
#include sys/stat.hmode_t umask(mode_t mask); //修改权限掩码3.write、read
#include unistd.hssize_t write(int fd, const void* buf, size_t count);fd文件描述符open 函数的返回值。buf指向要写入的数据的指针。count指定了要写入的字节数。返回值成功时返回真实写入文件的字节数失败时返回-1 FILE* fp fopen(log.txt, w); //低层就是下面的系统调用
int fd open(log.txt, O_CREAT | O_WRONLY | O_TRUNC, 0666);FILE* fp fopen(log.txt, a); //低层就是下面的系统调用
int fd open(log.txt, O_CREAT | O_WRONLY | O_APPEND, 0666);文本写入 VS 二进制写入 #includeunistd.hssize_t read(int fd, void* buf, size_t count);fd文件描述符。buf将数据读入到该指针 buffer 指向的字符串中。count需要读取的字节数。返回值成功时返回读取的字节数失败时返回-1 4.文件描述符
1.是什么 文件描述符就是从 0 开始的小整数。当我们打开文件时操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用所以必须让进程和文件关联起来。每个进程都有一个指针 *files指向一张表files_struct该表最重要的部分就是包含一个指针数组每个元素都是一个指向打开文件的指针本质上文件描述符就是该数组的下标。所以只要拿着文件描述符就可以找到对应的文件。 文件描述符进程对应的文件描述符表的数组下标。当用户层进行 open(“log.txt”, “w”) 调用时操作系统创建新的 struct file在文件描述符表中找到未被使用的下标将 struct file 的地址填写进去此时进程与文件就关联了。当用户层进行 read(fd, buffer, sizeof(buffer)) 调用时操作系统拿着 fd 索引文件描述符表找到对应的 struct file每一个 struct file 都对应内存中的一个 “文件缓冲区”操作系统先将磁盘文件中的内容预加载到文件缓冲区中再将文件缓冲区中的内容拷贝到 buffer 中。read 函数本质内核到用户空间的拷贝函数。当用户层进行 write(fd, buffer, strlen(buffer)) 调用时先将 buffer 指向的内容拷贝到文件缓冲区中再将缓冲区中的内容定期刷新到磁盘文件中。对文件做任何操作都必须先将文件加载磁盘-内存的拷贝到内核对应的文件缓冲区中。 内核代码如下
通过 open 系统调用的返回值得知文件描述符fd是一个整数。 如下代码所示 思考文件描述符为什么从3开始值为0、1、2 的文件描述符是什么 文件描述符值为0、1、2 分别是标准输入、标准输出、标准错误。C语言中的 fopen 返回值 FILE* 中的 FILE 是一个结构体。在操作系统接口层面上只认文件描述符 fd结构体 FILE 一定封装了文件描述符 fd 封装 在 Windows、Linux 等不同的平台下的系统调用不同使用系统调用不具备可移植性。C/C封装各个平台关于文件操作的系统调用在不同的平台下通过条件编译进行裁剪成为语言级接口具备可移植性。语言增加可移植性的原因为了让更多人使用防止被淘汰。 2.分配规则
文件描述符的分配规则在 struct file* array[] 数组当中找到当前没有被使用的最小的一个下标作为新的文件描述符。 3.重定向原理 解释图如下 printf 函数就是往 stdout 文件中打印内容也就是文件描述符值为 1 的文件但修改了 fd_array[1] 的指向时便打印到了 log.txt 文件中。更改文件描述符表中 fd_array[] 数组某个下标内容指针的指向数组下标不变叫做 “重定向” 4.通过dup2系统调用重定向
#include unistd.hint dup2(int oldfd, int newfd);作用makes newfd be the copy of oldfd, closing newfd first if necessary 5.标准错误重定向 思考都是输出到显示器中为什么要区分标准输出printf、cout和标准错误perror、cerr / 为什么存在标准错误答案标准输出和标准错误占用不同的文件描述符。虽然都是显示器但是可以通过重定向将常规消息和错误消息分离。 将标准输出和标准错误都重定向到一个文件中该如何做呢 6.自定义shell添加重定向功能 如果内建命令做重定向需要更改 shell 的标准输入、输出、错误。此时需要创建临时文件类似两个整数交换的过程进行一次重定向后需要恢复。一个文件可以被多个进程打开。若一个进程将其中一个文件关闭就会影响其它正在读取该文件的进程。所以 struct file 中有一个 ref_count引用计数的整形变量操作系统打开时引用计数为0指针指向该文件时引用计数关闭文件时引用计数–当引用计数为0时struct file 被释放。 #includeiostream
#includecstdio
#includecstdlib
#includecstring
#includeunistd.h
#includesys/types.h
#includesys/wait.h
#includeunordered_map
#includectype.h
#includesys/stat.h
#includefcntl.h#define COMMAND_SIZE 1024
#define FORMAT [%s%s %s]# //命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc 0;//环境变量表
#define MAX_ENVS 128
char* g_env[MAX_ENVS];
int g_envs 0;//别名映射表
std::unordered_mapstd::string, std::string alias_list;//重定向关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3int redir NONE_REDIR;
std::string filename;//for test
char cwd[1024];
char cwdenv[1024];//last exit code
int lastcode 0;void InitEnv()
{extern char** environ;memset(g_env, 0, sizeof(g_env));g_envs 0;//1.获取环境变量for(int i 0; environ[i]; i){//申请空间g_env[i] (char*)malloc(strlen(environ[i] 1));//拷贝环境变量strcpy(g_env[i], environ[i]);g_envs;}g_env[g_envs] (char*)XZY123456;g_env[g_envs] NULL;//2.导入环境变量for(int i 0; g_env[i]; i){putenv(g_env[i]);}environ g_env;
}//获取用户名
const char* GetUserName()
{const char* name getenv(USER);return name NULL ? None : name;
}//获取主机名
const char* GetHostName()
{const char* name getenv(HOSTNAME);return name NULL ? None : name;
}//获取当前路径
const char* GetPwd()
{//const char* pwd getenv(PWD); 根据环境变量PWD获得当前路径(当cd修改路径时环境变量不会被修改)const char* ret getcwd(cwd, sizeof(cwd)); //通过系统调用getcwd获取当前路径cwdif(ret ! NULL) //获取成功时ret也是当前路径{snprintf(cwdenv, sizeof(cwdenv), PWD%s, cwd); //格式化环境变量PWDcwdputenv(cwdenv); //更新环境变量PWDcwd}return ret NULL ? None : cwd;
}//获取家目录
const char* GetHome()
{const char* home getenv(HOME);return home NULL ? NULL : home;
}//根据当前绝对路径修改为相对路径
std::string DirName(const char* pwd)
{
#define SLASH /std::string dir pwd;if(dir SLASH) return SLASH;auto pos dir.rfind(SLASH);if(pos std::string::npos) return BUG?;return dir.substr(pos 1);
}//制作命令行提示符
void MakeCommandLinePrompt(char prompt[], int size)
{//snprintf(prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str()); snprintf(prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}//打印命令行提示符
void PrintCommandLinePrompt()
{char prompt[COMMAND_SIZE];MakeCommandLinePrompt(prompt, sizeof(prompt));printf(%s, prompt);fflush(stdout);
}//获取命令
bool GetCommand(char* command, int size)
{char* ret fgets(command, size, stdin);if(ret NULL) return false;command[strlen(command) - 1] \0; //清理\nif(strlen(command) 0) return false;return true;
}//命令解析
bool CommandPrase(char* command)
{
#define SEP g_argc 0;g_argv[g_argc] strtok(command, SEP);while((bool)(g_argv[g_argc] strtok(NULL, SEP)));g_argc--;return g_argc 0 ? true : false;
}//打印命令行参数
void PrintArgv()
{for(int i 0; g_argv[i]; i){printf(argv[%d]%s\n, i, g_argv[i]);}printf(argc%d\n, g_argc);
}//父进程执行cd命令
bool Cd()
{if(g_argc 1){std::string home GetHome();if(home.empty()) return true;chdir(home.c_str());}else {std::string where g_argv[1];if(where ~){}else if(where -){}else {chdir(where.c_str());}}return true;
}bool Echo()
{if(g_argc 2){std::string opt g_argv[1];if(opt $?) //echo $?{std::cout lastcode std::endl;lastcode 0;}else if(opt[0] $){std::string env_name opt.substr(1);const char* env_value getenv(env_name.c_str());if(env_value)std::cout env_value std::endl;}else {std::cout opt std::endl;}}return true;
}//检测并执行内建命令由父进程执行
bool CheckAndExecBuiltin()
{std::string cmd g_argv[0];if(cmd cd){Cd();return true;}else if(cmd echo){Echo();return true;}else if(cmd export) {//1.在环境变量表中查找环境变量名是否存在//2.存在修改不存在新增}else if(cmd alias){//std::string nickname g_argv[1];//alias_list.insert(k, v)}return false;
}//执行普通命令由子进程执行
void ExecuteCommand()
{ pid_t id fork();if(id 0){//子进程//子进程检查重定向情况父进程不能重定向int fd -1;if(redir INPUT_REDIR){fd open(filename.c_str(), O_RDONLY);if(fd 0) exit(1);dup2(fd, 0);close(fd);}else if(redir OUTPUT_REDIR){fd open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd 0) exit(2);dup2(fd, 1);close(fd);}else if(redir APPEND_REDIR){fd open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd 0) exit(2);dup2(fd, 1);close(fd);}else {}//进程替换不影响重定向的结果execvp(g_argv[0], g_argv);exit(1);}//父进程int status 0;pid_t rid waitpid(id, status, 0);if(rid 0){lastcode WEXITSTATUS(status);}
}void TrimSpace(char command[], int end)
{while(isspace(command[end])){end;}
}void RedirCheck(char command[])
{ redir NONE_REDIR;filename.clear();int start 0, end strlen(command) - 1;while(end start){if(command[end] ){command[end] \0;TrimSpace(command, end);redir INPUT_REDIR;filename command end;break;}else if(command[end] ){if(command[end - 1] ){command[end - 1] \0;end;TrimSpace(command, end);redir APPEND_REDIR;filename command end;break;}else {command[end] \0;TrimSpace(command, end);redir OUTPUT_REDIR;filename command end;break;}}else {end--;}}
}int main()
{//shell启动的时候需用从系统中获取环境变量//我们的环境变量信息应该从父shell中获取InitEnv();while(true){ //1.输出命令行提示符PrintCommandLinePrompt();//2.获取用户输入的命令char command[COMMAND_SIZE];if(!GetCommand(command, sizeof(command)))continue;//3.重定向分析ls -a -l file.txt - ls -a -l file.txt - 判定重定向方式RedirCheck(command); //printf(redir %d, filename %s\n, redir, filename.c_str());//4.命令解析ls -a -l - ls、-a、-l if(!CommandPrase(command))continue;//PrintArgv();//检测别名//5.检测并处理内建命令if(CheckAndExecBuiltin())continue;//6.执行命令ExecuteCommand();}return 0;
}5.理解一切皆文件 首先在windows中是文件的东西它们在linux中也是文件。其次一些在windows中不是文件的东西进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件你可以使用访问文件的方法访问它们获得信息。甚至管道也是文件网络编程中的socket套接字这样的东西使用的接口跟文件接口也是一致的。这样做最明显的好处是开发者仅需要使用一套 API 和开发工具即可调取 Linux 系统中绝大部分的资源。举个简单的例子Linux 中几乎所有读读文件读系统状态读 PIPE的操作都可以用 read 函数来进行。几乎所有更改更改文件更改系统参数写 PIPE的操作都可以用 write 函数来进行。当打开一个文件时操作系统为了管理所打开的文件都会为这个文件创建一个 file 结构体该结构体定义在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下以下展示了该结构部分我们关系的内容 struct file
{//...struct inode* f_inode; /* cached value */const struct file_operations* f_op;//...atomic_long_t f_count; //表示打开文件的引用计数如果有多个文件指针指向它就会增加f_count的值unsigned int f_flags; //表示打开文件的权限fmode_t f_mode; //设置对文件的访问模式例如只读只写等。loff_t f_pos; //表示当前读写文件的位置 //...
};值得关注的是 struct file 中的 f_op 指针指向了一个 file_operations 结构体这个结构体中的成员中存在 read 和 write 等函数指针。如下 struct file_operations
{//...ssize_t(*read) (struct file*, char __user*, size_t, loff_t*);ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*);//...
};file_operation 就是把系统调用和驱动程序关联起来的关键数据结构这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针接着把控制权转交给函数从而完成了Linux设备驱动程序的工作。一张图总结如下 上图中的外设每个设备都可以有自己的 read 和 write但对应着不同的操作方法通过 struct file 下 file_operation 中的各种函数回调让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源这便是 “linux下一切皆文件” 的核心理解。 6.缓冲区
1.什么是缓冲区 缓冲区内存中预留了一段存储空间这些空间用来缓冲输入或输出的数据该空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备分为输入缓冲区和输出缓冲区。 以上是什么导致的呢 通过C语言中的库函数printf/fprintf/fputs/fwrite输出数据并不是直接写到文件内核缓冲区中若是直接写到文件内核缓冲区中那么进程关闭该文件时会将缓冲区中的内容刷新到外设中上面的代码并未出现该结果。而是在C语言的标准库中它为每一个打开的文件创建一个用户层语言级缓冲区。通过系统调用write输出数据直接刷新到文件内核缓冲区中。当用户强制刷新 / 筛选条件满足 / 进程退出时由C标准库根据文件描述符fd 系统调用write将语言级缓冲区中的内容刷新到文件内核缓冲区中。在调用 close 之前进程还未退出即没有强制刷新 / 筛选条件满足 / 进程退出数据会一直在C标准库中的语言级缓冲区。当调用 close 时文件描述符 fd 被关闭然后进程退出了此时打算将语言级缓冲区中的内容刷新到文件内核缓冲区但是调系统调用时 fd 被关了无法刷新到文件内核缓冲区就无法看见内容。 2.FILE 问题每一个文件都有自己对应的语言级缓冲区那么语言级缓冲区在哪里呢答案FILE 是 C 语言中的结构体其中封装了文件描述符fd和语言级缓冲区 如下是FILE的部分内容
typedef struct _IO_FILE FILE;struct _IO_FILE
{int _fileno; //封装的文件描述符//缓冲区相关//...char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putbackget area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. *///...
};3.缓冲类型 全缓冲要求填满整个缓冲区后才进行 I/O 系统调用操作。对于磁盘文件的操作通常使用全缓冲。行缓冲当在输入和输出中遇到换行符时标准 I/O 库函数将会执行系统调用操作。对于显示器通常使用行缓冲。无缓冲标准 I/O 库不对字符进行缓存直接调用系统调用。标准出错流 stderr 通常是不带缓冲区的这使得出错信息能够尽快地显示出来。 数据交给系统交给硬件的本质全是拷贝。计算机流动的本质一切皆拷贝 4.为什么要引入缓冲区机制 读写文件时如果不会开辟对文件操作的缓冲区直接通过系统调用对磁盘进行读、写等操作那么每次对文件进行一次读写操作时都需要使用读写系统调用来处理此操作即需要执行一次系统调用执行一次系统调用将涉及到CPU状态的切换即从用户空间切换到内核空间实现进程上下文的切换这将损耗一定的CPU时间频繁的磁盘访问对程序的执行效率造成很大的影响。为了减少使用系统调用的次数提高效率我们就可以采用缓冲机制。比如我们从磁盘里取信息可以在磁盘文件进行操作时可以一次从文件中读出大量的数据到缓冲区中以后对这部分的访问就不需要再使用系统调用了等缓冲区的数据取完后再去磁盘中读取这样就可以减少磁盘的读写次数再加上计算机对缓冲区的操作快于对磁盘的操作故应用缓冲区可大大提高计算机的运行速度。又比如我们使用打印机打印文档由于打印机的打印速度相对较慢我们先把文档输出到打印机相应的缓冲区打印机再自行逐步打印这时我们的CPU可以处理别的事情。可以看出缓冲区就是一块内存区它用在输入输出设备和CPU之间⽤来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作避免低速的输入输出设备占用CPU解放出CPU使其能够高效率工作。多次执行 printf 函数可能内部只执行一次系统调用 write可以减少系统调用的次数提高效率。 5.设计文件libc库