Linux 基础IO
文件周边知识
- 文件=内容+属性;对文件的操作,即对文件的内容和属性的操作;空文件也占磁盘空间
- 文件默认在磁盘中;一个进程可以通过操作系统提供的系统调用接口将多个磁盘中的文件加载到内存里
- 没被打开的文件在磁盘中,内存中为被打开的文件
- 文件要被打开,一定会在内核中形成被打开文件对象
- 操作系统通过先描述再组织的方法,管理这些被打开的文件
常见的C语言的文件接口
//FILE* fp = fopen("log.txt", "w"); FILE* fp = fopen("log.txt", "a"); if(fp == NULL) { perror("fopen"); return -1; } const char* str = "Hello Linux file\n"; fputs(str, fp); fclose(fp);
w模式打开文件
- 不存在则创建,清空写入
a模式打开文件
- 不存在则创建,追加写入
r
- 文件不存在报错
带上+
- r+,w+,a+,都变成读写模式
strerror和perror
strerror
返回指向描述错误信息的字符串的指针,需要用户自己处理输出perror
直接输出错误信息到标准错误流,包含用户提供的字符串和系统错误信息perror
更简便于直接输出错误信息,而strerror
提供更灵活的错误描述获取方式,适合需要自定义输出格式的场景
> log.txt
- 将标准输出重定向到指定的文件
- 以w的方式打开文件,意味着文件将被清空
- 没有前置命令,所以什么都没有输出到文件,就把文件关了
系统调用接口
open
头文件
- sys/types.h
- sys/stat.h
- fcntl.h
返回值
- 返回一个整数,表示文件描述符(fd)
- 文件描述符是一个非负整数,用于标识已打开的文件
- 如果函数调用失败,返回
-1
,并设置errno
来指示具体的错误
pathname
- const char *
- pathname:文件路径+文件名;若没有指明路径,则是当前工作目录下的文件
flags
- 文件打开的方式,按比特位传参,可以组合多个标志位;用 按位或 连接
O_RDONLY
:只读打开文件O_WRONLY
:只写打开文件O_RDWR
:读写方式打开文件O_CREAT
:如果文件不存在则创建文件(需要mode
参数)O_TRUNC
:打开文件时将文件长度截断为0(清空文件内容)O_APPEND
:以追加模式打开文件,写操作从文件末尾开始
flags类似原理
#include <stdio.h> #define OPTION1 1
#define OPTION2 (1<<1)
#define OPTION3 (1<<2)
#define OPTION4 (1<<3) void choice(int options)
{ if(options&OPTION1) printf("OPTION1\n"); if(options&OPTION2) printf("OPTION2\n"); if(options&OPTION3) printf("OPTION3\n"); if(options&OPTION4) printf("OPTION4\n"); printf("\n");
} int main()
{ choice(OPTION1); choice(OPTION2|OPTION3); choice(OPTION3|OPTION4); choice(OPTION1|OPTION2|OPTION3|OPTION4); return 0;
}
mode
- 仅在使用
O_CREAT
标志时需要,若没有则生成的文件有问题 - 例:0666;mode&(~umask)
- 中间出现了过open: Permission denied这样的错误,rm -f log.txt就行了;;知道为什么不允许了,开始没加mode运行,那么这时文件已创建,但是拥有者没有写的权限,导致open不被允许
read(没写)
- 将文件描述符中的数据读取到buf缓冲区中,读取的字节数为count
返回值
- 返回值 > 0:表示成功读取的字节数
- 返回值 == 0:表示已经到达文件的末尾,没有更多的数据可读取
- 返回值 == -1:表示读取过程中发生错误。错误可能是由于文件描述符无效、读权限问题
解释返回值为0的情况
- 打开文件时,文件指针从文件开头开始
- 每次 read() 操作都会从文件指针当前位置读取并向后移动
- 当文件指针到达 EOF (文件末尾)后,再次 read() 会返回 0,表示没有更多数据可以读取
write
- 用于将缓冲区中的数据写入到文件或设备,该文件或设备由文件描述符指定,用于操作系统与用户程序之间进行数据交互
fd
- 文件描述符,用于指定数据写入的文件
- 0:stdin 键盘
- 1:stdout 显示器
- 2:stderr 显示器
buf
- 指向要写入的数据的缓冲区的指针
count
- 从缓冲区写入文件或设备的字节数
返回值
- 成功时,返回实际写入的字节数,这个值可能小于
count
,例如发生错误或系统调用被中断时(仍被视为成功) - 失败时,返回
-1
,并设置errno
以指示具体的错误原因。
- ??为什么打印的是 Hello Li??
- 因为这里sizeof msg 拿到的是指针的大小
- 低8行,mode写成666不对,要改成0666;如图:权限不符合预期
语言与系统的角度理解文件
fd的顺序测试
- 启动进程的时候会先加载3个文件描述符
- 每个进程都有自己的独立的stdin,stdout,stderr,文件描述符在每个进程的上下文中是独立的,指向相同的设备,看起来像是在“共享”同一份输入输出,但实际上是独立管理的
open过程
- 磁盘中要被打开的文件加载到内存中,加载的形式是通过创建file对象来实现的
- 然后将file对象的地址填入files_struct对象 的fd_array的指针数组中
- 最后将对应的下标返回给open,进程通过这个文件描述符进行文件操作
write过程(补充)
- 进入了内核空间,通过系统调用的方式请求操作系统执行写入操作
- 找到PCB里的file指针
- 通过file指针,找到files_struct对象
- 通过files_struct对象里的fd_array
- fd_array数组里的file对象地址,通过文件描述符来找到对应的文件对象
- 执行写入
总结
- 文件描述符的本质就是数组的下标,有了fd这个数组,使得进程与文件实现解耦,用这张表建立映射关系;
- 文件描述符是进程访问文件的基本形式
- 操作系统只能通过文件描述符访问文件
C语言的FILE和系统级别的file不是一回事(补充)
FILE
通过封装底层file
对象,提供了一个更高层次、更易于使用的文件操作接口- FILE结构体内部包含一个文件描述符
为什么要默认打开0 1 2
- 于程序员:不需要手动打开或管理这些基本的 I/O 通道,简化程序设计
- 于管道和重定向:可以轻松地将标准输入、输出和错误重定向到文件、设备或其他进程
- 将错误信息通过 stderr 输出,使得错误消息与正常输出分离,有助于错误的识别和调试。 即使标准输出被重定向,错误信息仍然可以显示在控制台上,避免错误信息丢失
stdout与stderr分离(补充stderr stdout)
- 默认情况下都会显示到控制台上,输出位置相同没有什么区别
- 但是比方说./example > output.txt;;这个时候会重定向stdout,但不会重定向stderr
我对读和写有点不理解,老师说键盘是只读的,这个读指的是读外设的键盘;显示器是只写的,那么为什么呢?写的时候不应该先读吗?那不就有读的操作吗?为什么说显示器是只写的呢?
键盘
struct file
- 文件被访问之前要先被打开;打开之前要把文件加载到内存里;OS通过创建struct_file对象来管理文件
- struct本质并不存在,是文件被打开时在内核中创建的一个节点,专门用来管理被打开的文件
字段
- f_count:文件的引用计数,有几个指针指向这个文件,(FILE里有指针指向这个struct file)
- f_flags:文件的打开方式
- f_mode:权限
- f_pos:打开文件时的读写位置,包括这个文件是谁打开的
- f_mapping:与文件的缓冲区相关,将磁盘上的数据与内存(内核缓冲区)映射
- file_operations* f_op:包含了很多函数指针
- f_dentry->d_inode:找到对应的磁盘文件
读写
- 读数据:磁盘中的文件加载到对应的内核缓冲区里
- 写数据:若缓冲区里没数据,则会发生缺页中断,OS会将磁盘中的数据唤入
- 以前写的不对,如果是追加写,那么读写之前都要先唤入磁盘中的数据到对应fd的内核缓冲区中,如果内核缓冲区中的数据没有,就会触发缺页中断,强行去唤入磁盘中的数据
- 比方说c库的fread,它最终也是从struct file里的缓冲区拿的数据
- 缺页中断对内核的虚拟地址空间有用,对程序的虚拟地址空间也有用
fd的分配规则
11 int fd = open(FILENAME, O_CREAT|O_WRONLY|O_TRUNC, 0666);12 if(fd < 0)13 {14 perror("open");15 return -1;16 }17 printf("fd: %d\n", fd);18 close(fd);
- 正常打开,不操作文件,fd = 3
11 char buffer[1024];12 ssize_t s = read(0, buffer, 1024);13 if(s > 0)14 {15 buffer[s] = '\0';16 write(1, buffer, strlen(buffer));17 }
- read:将stdin里的数据拷贝到buffer里
- write:将buffer里的数据拷贝到stdout里
- 实验说明:不需要手动打开0,1文件即可使用,OS会在进程启动时帮我们打开0,1文件
11 close(0);12 int fd = open(FILENAME, O_CREAT|O_TRUNC|O_WRONLY, 0666);13 if(fd < 0)14 {15 perror("open");16 return 1;17 }18 printf("fd = %d\n", fd);19 close(fd);
- 关闭0,fd为0,关闭2,fd为2;
- 分配规则:fd为最小的且没有被使用的数组的下标
- 当一个进程启动时,内核会为该进程 自动创建 文件描述符
0
、1
、2
,并为这些文件描述符分别创建对应的file
对象。这些file
对象代表标准输入、标准输出和标准错误,通常指向终端、管道、文件或其他设备
标准输入、标准输出和标准错误和标准输入流、标准输出流和标准错误流,有什么区别
- 前者是内核管理的
file
对象,通过文件描述符进行低级别操作(直接操作内核数据结构,更接近硬件和操作系统内核的层次) - 是用户态的
FILE
结构体,通过文件描述符提供高级 I/O 功能
重定向
重定向写
11 close(1); 12 int fd = open(FILENAME, O_CREAT|O_TRUNC|O_WRONLY); 13 if(fd < 0) 14 { 15 perror("open"); 16 return 1; 17 } 18 //printf("fd = %d\n", fd); 19 //printf("stdout_fd = %d\n", stdout->_fileno);20 fprintf(stdout, "fd = %d\n", fd);21 fprintf(stdout, "stdout_fd = %d\n", stdout->_fileno); 22 fflush(stdout); 23 close(1);
- 实验结果:打印到log.txt文件里
- close(1)之后,新打开的文件的fd变为了1;打印到log.txt里上层并不知道,printf只认stdout,更准确的是只认文件描述符1
close(1)释放了什么
- 仅仅是关闭了文件描述符1,file对象没有被销毁,只是切断了stdout与文件描述符之间的联系
- 上面是以前写的,其实并不是,它关闭的是fd_array里的一个指针,因为少了这个指针,所以对应的file对象里的f_count会减一,但是只有减到0,file对象才会释放,那么为什么没有减到0,因为C库里的FILE里面有一个指向file的指针!!!
- 所以他只不过是给对应的fd_array里的对应位置置空
- 此时标准输出的资源被释放:指的是断开了链接stdout所依赖的底层资源;如果此时没有重新建立连接,那么输出操作将无法执行或会报错
- 只要有FILE,他指向的file对象的f_count起码是2(FILE对象里一个,fd_array里一个)
- FILE对象里有一个指向file对象的指针,这个指针是从fd_array对应的fd里来的,
file对象的销毁(引用计数)
- file对象不是通过close销毁,而是通过引用计数(f_count)
- 当文件被打开,或者被dup*系统调用复制时,引用计数会增加
- close本身不会销毁file对象,而是减少引用计数,当f_count为0时,内核就会认为该 file 对象不再被使用,因此触发清理机制,释放file对象所占用的资源,真正销毁该对象
FILE与file
struct file * fd_array[NR_OPEN_DEFAULT]; 2.6.32.19源代码
★stdin,stdout,stderr是FILE类型的指针(并不是FILE对象)底层封装了文件描述符0,1,2
fd_array的0,1,2位置处存储的是输入输出设备或文件(这些都是用file对象维护)的地址
FILE结构体里没有file对象的实体,而是包含一个文件描述符(或指针),通过文件描述符间接与内核中的实际文件进行交互
用户态下位FILE结构体;内核态下为file对象
我们是很难关闭012对应的struct file对象,因为别的进程也在用它,你怎么关?别的不说,当前进程是由bash创建的,也是就说bash012不关,对应的file对象永远关不掉
其二是文件描述符表里多一个数据,那么这个数据对应的file对象的f_count就会++
那我问你,一个进程里有3个FILE都操作同一个file对象,那么这个file对象里的f_count有几个,我觉得至少是4个 gpt赞同
好,我继续问你,如果这时候,将fd=4里的内个改成fd = 1内容,那么1对应的file的f_count+4,而4对应的file对象的f_count-4,你有异议吗 ---gpt赞同,我的理解没问题
再说一点:为什么file对象只有一个:file对象就是一个出口,对于整个系统而言,出口一个就够了,一定有多个入口,多个进程里的FILE能够发送数据(当然有锁)
多说一嘴逻辑:如果file对象本来就有,那么这个进程对应的fd里的内容直接填file的地址就行了,如果没有这个file,创建即可,OS当然知道有没有了,其实所有进程的012都是一个地址,也就是同一个file对象
不加fflush为什么不能拷贝到FILENAME里?
- 的作用是将 C 语言的
FILE
结构体(即stdout
)所管理的用户态缓冲区中的数据直接写入到文件描述符1
当前指向的file
对象 - 但是如果把close去掉又是可以写入的,因为在程序结束时,C 标准库会自动刷新
stdout
的缓冲区,将数据写入文件,再关闭被写入文件
写重定向
11 close(0);12 int fd = open(FILENAME, O_RDONLY);13 if(fd < 0)14 {15 perror("open");16 return 1;17 }18 char buffer[1024];19 //fread(buffer, 1, sizeof buffer, stdin);20 read(0, buffer, sizeof buffer); 21 printf("%s", buffer);
- 改变文件描述符0对应的内容
重定向本质
- 更改文件描述符(如标准输入、标准输出和标准错误)所指向的文件或设备,使得数据流被导向新的目标
- 上层fd不变,底层fd指向的内容改变
dup2
- 不通过fd的分配规则完成重定向,用dup2系统调用
- 用于复制文件描述符的系统调用,将一个文件描述符复制到另一个指定的文件描述符位置,从而使两个文件描述符指向同一个 file 对象
oldfd
:要复制的源文件描述符。newfd
:目标文件描述符,oldfd
将被复制到此处。- 返回值:成功时返回
newfd
,失败时返回-1
并设置errno
。
重定向原理
- 文件描述符表中数组内容的拷贝
重定向写
11 int fd = open(FILENAME, O_CREAT|O_TRUNC|O_WRONLY, 0666); 12 if(fd < 0)13 {14 perror("open");15 return 1;16 }17 dup2(fd, 1);18 printf("fd = %d", fd);
重定向读
11 int fd = open(FILENAME, O_RDONLY);12 if(fd < 0)13 { 14 perror("open");15 return 1;16 }17 dup2(fd, 0);18 char buffer[1024];19 //fread(buffer, 1, sizeof buffer, stdin);20 read(fd, buffer, sizeof buffer);21 printf("%s", buffer);
mybash添加重定向功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>#define COMMAND_LEN 1024
#define ARGV_LEN 1024
#define STRTOK_CH " "
#define DEBUG 1
//const char* STRTOK_CH " ";
#define NONEREDIR 0
#define INPUTREDIR 1
#define OUTPUTREDIR 2
#define APPENDREDIR 3int redir = NONEREDIR;
char* filename = NULL;
int lastcode = 0;void printCommand()
{printf("[%s@%s %s]# ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
}void getCommand(char command[])
{gets(command);
}int splitCommand(char command[], char* argv[])
{int argc = 0;argv[argc++] = strtok(command, STRTOK_CH);//第二个参数要是字符串while(argv[argc++] = strtok(NULL, STRTOK_CH));
//#ifdef DEBUG
// for(size_t i = 0; argv[i]; i++)
// printf("%s\n", argv[i]);//为什么不打印?
//#endif //因为return 写到ifdef上面了return argc;
}
void excuteSubProcess(char* argv[])
{pid_t id = fork();if(id < 0){return;}if(id == 0){int fd = 0;if(redir == INPUTREDIR){fd = open(filename, O_RDONLY);dup2(fd, 0); }else if(redir == OUTPUTREDIR){fd = open(filename, O_TRUNC|O_CREAT|O_WRONLY, 0666);dup2(fd, 1);}else if(redir == APPENDREDIR){fd = open(filename, O_APPEND|O_CREAT|O_WRONLY, 0666);dup2(fd, 1);}execvp(argv[0], argv);}else{int status = 0;pid_t rid = waitpid(id, &status, 0);//if(WIFEXITED(status) == 0) 这样不行啊,确实,看底层实现就明白if(WIFEXITED(status))//不对,再看//if(rid > 0){lastcode = WEXITSTATUS(status);}}
}
char cwd[1024];
void cd(char* path)
{chdir(path);char tmp[1024];getcwd(tmp, sizeof tmp);sprintf(cwd, "PWD=%s", tmp);//下面这样会乱码,为什么 //getcwd(cwd, sizeof cwd);//sprintf(cwd, "PWD=%s", cwd);这个函数是直接往cwd里写,导致前缀被覆盖putenv(cwd);
}char myenv[100];
int dobuildin(char* argv[])
{if(strcmp(argv[0], "cd") == 0){char* path = NULL;if(argv[1]) path = argv[1];else path = getenv("HOME");cd(path);return 1;}else if(strcmp(argv[0], "export") == 0){if(argv[1] == NULL) return 1;strcpy(myenv, argv[1]);putenv(myenv);return 1;}else if(strcmp(argv[0], "echo") == 0){//#ifdef DEBUG
// for(size_t i = 0; argv[i]; i++)
// printf("IFDEF:%s\n", argv[i]);//为什么不打印?
//#endif //因为return 写到ifdef上面了//printf("%s\n",filename );int fd = 0;if(redir == INPUTREDIR){fd = open(filename, O_RDONLY);dup2(fd, 0); }else if(redir == OUTPUTREDIR){fd = open(filename, O_TRUNC|O_CREAT|O_WRONLY, 0666);printf("fd%s\n",filename );dup2(fd, 1);printf("dup2%s\n",filename );}else if(redir == APPENDREDIR){fd = open(filename, O_APPEND|O_CREAT|O_WRONLY, 0666);dup2(fd, 1);}//printf("%s\n",filename );if(argv[1] == NULL){printf("\n");return 1; //必须要return 1 不然下面的else没意义了}if(*argv[1] == '$' && strlen(argv[1]) > 1){char* val = argv[1] + 1;if(strcmp(val, "?") == 0) //原来是这里 ,弄的我以为argv[1]为0{printf("%d\n", lastcode);lastcode = 0;}else{const char* _env = getenv(val);//const char* _env = getenv(argv[1]);if(_env) printf("%s\n", _env);else printf("\n");}}else {printf("%s\n", argv[1]);}return 1;}return 0;
}#define SKIPSPACE(pos) do{ while(*pos == ' ') pos++; }while(0)
void checkRedir(char command[])
{char* start = command;char* end = command + strlen(command) - 1;while(start < end){if(*end == '>'){if(end - start > 0 && *(end - 1) == '>'){redir = APPENDREDIR;*(end - 1) = '\0';// filename = end + 1;// SKIPSPACE(filename);++end; SKIPSPACE(end);//SKIPSPACE(++end);这样不行,这样整个pos都会变成++end,因为只是宏filename = end;break;}else {redir = OUTPUTREDIR;*end = '\0';filename = end + 1;SKIPSPACE(filename);break;}}else if(*end == '<'){redir = INPUTREDIR;*end = '\0';filename = end + 1;SKIPSPACE(filename);break;}else{end--;}}
}
//void changeRedir() //这就是个bug,这里函数结束就被关了
//{
// int fd = 0;
// if(redir == INPUTREDIR)
// {
// fd = open(filename, O_RDONLY);
// dup2(fd, 0);
// }
// else if(redir == OUTPUTREDIR)
// {
// fd = open(filename, O_TRUNC|O_CREAT|O_WRONLY, 0666);
// dup2(fd, 1);
// }
// else if(redir == APPENDREDIR)
// {
// fd = open(filename, O_APPEND|O_CREAT|O_WRONLY, 0666);
// dup2(fd, 1);
// }
//}
int main()
{while(1){redir = NONEREDIR;filename = NULL;char command[COMMAND_LEN];printCommand();getCommand(command);checkRedir(command);//changeRedir();char* argv[ARGV_LEN];int n = splitCommand(command, argv);if(n <= 0) continue;n = dobuildin(argv);if(n == 1) continue;excuteSubProcess(argv);}
}
pwd、cwd、getcwd
遇到的问题、想法与思路
下面都是bug代码
1.为什么定义了DEBUG也不输出
- #ifdef DEBUG是一个条件编译指令,只检查DEBUG是否被定义
- 写这个宏的目的是检查有没有argc是否有接收到
- 错误就在于打印之前被return了
2.WIFEXITED(status)的相关问题
- 在不知道底层实现前,我认为他是通过判断次8位,即就是检查退出码的位置,因为它的名字WIFEXITED
- 结果实际上,是检查异常的7位
- 同时也纠正之前的点,异常不一定是自己写出来的,参数传不对,导致执行失败也是异常
((status) & 0x7F) == 0;这个其实通过检查异常是否为0来侧面反应进程是否正常退出;因为异常为0,那么就一定是正常退出,这时候可以拿退出码
有退出码就是正常退出
3.为什么注释的代码不行
- 问题在于对函数的理解不到位
- 如果按照下面这么写,执行次数多了,会有很多的PWD=这样的串
- 这是因为这个函数是直接往cwd里写,不会先拷贝到一个缓冲区,导致前缀被覆盖
4.★发现echo hello > log.txt执行不了
- 代码历程:开始只有excuteSubProcess函数里执行了改流操作
- 所以我想了,echo hello > log.txt这个是父进程去执行的,应该在他们俩之前改
- 所以我把改流操作封装成了个函数
- 就像这样,然后我把子进程中的改流操作删了,结果就是子进程也不行了,且出现了问题:在执行重定向的时候,它会等待我输入,之后好像是崩溃了,然后查看log.txt
内容怎么是这样的???奇怪为什么命令行提示符写到文件里了??
- 这时候的我还不知道为什么,想到了一个原因但不是主要原因:因为出了函数这个文件就被关了
- 所以我在父和子都加上了这个代码
- 结果:子进程正常,父进程卡主
- 会不会是filename空解引用呢。就加了个if(filename)
- 还是不行,然后通过定位打印发现,dup2之后的printf打印不出来
- 那么锁定了问题就在dup2,我仔细对照子进程那块,一模一样拷贝的啊,怎么会有问题呢?
- 原来是因为把父进程的输出流给改成文件了,所以问题都迎刃而解
为什么log.txt里有很多命令行提示符?
- 因为输出流改成了log.txt导致所有的包括printCommand函数里的printf打印到了log.txt里
为什么执行echo hello > log.txt会等待我们输入
- 因为输入流没有被改变,所以还会等待输入,只不过结果打印在log.txt里
为什么回车会报段错误?
- 这是因为gets不会读取 '\n' ,导致command没有数据,那么在strtok的时候就会报段错误,纠正如下
- 加个返回值,0就continue就可以了,反之进入strtok里报错
警告的原因
- :
gets()
函数从标准输入读取数据,但它无法控制输入的长度,也不会检查缓冲区是否有足够的空间来存储输入的数据 - 这会导致缓冲区溢出,可能引发程序崩溃,甚至带来安全漏洞(如恶意代码注入)。
- 由于这些问题,
gets()
被认为是极度不安全的,C11 标准中已经废弃了gets()
,并不再被包含在标准库中
5.SKIPSPACE宏的相关问题
- 注意宏的整体替换,如果是++end的话,那么上面的pos全会变成++end,直接就报错了
这样filename会被修改吗?
- 因为他是宏,所以会被修改;不是函数的形参
也可以这样实现
重定向不会影响程序替换
- 重定向:影响程序替换后新程序的输入和输出行为,是将标准输入、输出、或错误输出重定向到文件或其他流中
- 程序替换:用于将当前进程替换为一个新程序。exec 系列函数不会创建新进程,而是替换当前进程的代码和数据段
- 两者互不影响,替换前后的代码都会用到流,它们共同影响程序的输入输出流
VFS
stderr
通过操作来理解stderr的行为
1.
- 和stdout一样默认指向控制台
2.
- 说明了命令行中 > 只会重定向 输出流
3.
- 2>&1这样也可以重定向,但要注意格式
- ④这种操作是不行的,一个进程可以同时多次打开同一个文件(不close)
- 推荐用下面的形式,将stderr与stdout写到不同文件里,注意格式 (2> 这两个字符间没有空格,一次命令,多次重定向)
- ./a.out &> log.txt:将标准输出和标准错误都重定向到同一个文件
- cat log.txt就是cat < log.txt的缩写,这个像输入流的重定向,但实际不是:它的作用是将文件内容连接起来,并将结果显示在标准输出上;读取文件内容并输出,而不是更改输入或输出的方向
★./a.out > log.txt 2 > log.txt为什么没达到预期
- ./a.out > log.txt 2 > log.txt这样的话,stderr会打印到显示器上,所以推测2 > log.txt这个没用,可能是因为格式不对
- 将2与>之间的空格去掉,显示器上没有了,但是为什么文件里没有stderr???
- !!!!">" 这个会清空文件
- 那为什么是log.txt里是stderr??它不是后打开的吗
- 是的,它是后打开的,但它是先写入的,没想到吧
stderr
通常是无缓冲或行缓冲的,因此输出会立即显示,而stdout
是全缓冲的,可能会被暂存,直到缓冲区被刷新或程序结束时才输出。
补充
- 对应格式,不清楚shell是怎么读取的,但实验发现有两个重定向,后者的重定向符号与文件描述符之间不能有空格
- perror();cerr 默认写入的是stderr
缓冲区
- 缓冲区是一块内存区域,暂时存储数据,以提高传输效率
刷新方式
- 无缓冲(立即刷新):write
- 行缓冲(行刷新):显示器文件
- 全缓冲(缓冲区满刷新):磁盘文件
刷新策略
- 强制刷新(fflush)
- 进程退出的时候,刷新缓冲区
样例
1 #include <stdio.h>2 #include <unistd.h>3 #include <string.h>4 5 const char* str1 = "C library: fputs\n";6 const char* str2 = "system call: write\n";7 int main()8 {9 printf("C library: printf\n");10 fprintf(stdout, "C library: fprintf\n");11 fputs(str1, stdout);12 write(1, str2, strlen(str2));13 //fork(); 14 return 0;15 }
向显示器打印
- 显示器文件的刷新方式是行刷新,所以C library 的打印到用户缓冲区的时候直接被刷新到文件缓冲区,显示器效果如预期
向文件打印
- 文件数刷新方式是C缓冲区满了再刷新
- 程序的确是C library的先执行,但write系统调用绕过了用户空间的 C 库缓冲区,直接将数据写入内核空间的文件缓冲区
- 最后程序退出时,刷新缓冲区
带上fork()执行./mybin>log.txt
1.fork函数在IO函数后面,为什么也能被子进程继承?
- 子进程创建的时候,是拷贝的父进程,所以有与父进程相同的虚拟地址空间和页表,C缓冲区也在用户空间,其对应的虚拟地址和物理地址也在页表了,所以子进程也会刷新
2.为什么子进程比父进程先打印
- 因为子进程先退出
3. 我并不认为这里发生了缓冲区的写实拷贝
- 如果硬要说有,我觉得在子进程刷新的时候,数据被删除了也算写,修改就算写吧
4. 父子进程指向同一个用户空间上的缓冲区,子进程刷新完了,父进程怎么刷新?
- 缓冲区内容被写入到文件后,物理内存上的缓冲区内容并不会被自动清除或标记为空。只是文件流的状态被标记为“已刷新”
5. 标准输出处于行缓冲模式,子进程为什么不打印C library: fputs,不是说刷新了还在缓冲区上吗
- 子进程拷贝父进程的时候,文件流的状态也被拷贝了,所以这时候,即是缓冲区里有内容也不刷新