文章目录
复习C语言io知识
#include<stdio.h> int main() { //FILE* fp=fopen("./log.txt","r"); FILE* fp=fopen("./log.txt","a");//追加,不会覆盖掉 if(NULL==fp) { perror("fopen"); return 1; } #if // char buffer[32]; // while(fgets(buffer,sizeof(buffer),fp)!=NULL) // { // printf("%s", buffer); // } // if(!feof(fp)) // { // printf("fgets quit not normal\n"); // // } // else // { // printf("fgets quit normal\n"); // } int cnt=10; const char* msg="hello "; while(cnt--){ fputs(msg,fp); } fclose(fp); return 0; }
C 程序默认会打开3个输出流,stdin,stdout,stderr
stdin对应键盘,stdout对应显示器,stderr对应显示器
fputs(msg,stdout);//直接向显示器去写入
stdout是向显示器去输出
将原本应该显示到显示屏的内容,显示到了文件里面
本质是指把stdout的内容重定向到文件中
把原本应该打印在文件里面的打印在了显示器里面fputs向一般文件或者硬件设备都能写入
磁盘也是硬件
同理
c++:cin,cout cerr
c语言的一切操作实际上都是在向硬件写入(所有语言上对“文件”的操作都要贯穿操作系统)
即最终都是访问硬件,
用户行为–>语言,程序,lib–>OS–>驱动–>硬件
但是操作系统不相信任何人 ,所以访问操作系统是需要系统调用接口的
所以几乎所有的语言fopen,fclose,fread等等的底层一定是使用OS提供的系统调用
学习文件的系统调用接口
离OS更近,更能了解
文件打开
int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
pathname就是要打开的路径名,flags就是我们打开的方式,mode就是打开的权限信息,
会返回一个文件描述符,int类型
flag是整数,:传递标志位,
int:32个bit位,一个bit,就代表一个标志,就代表一个标志位
0000 0000,如以最后一个标志位为0还是1,代表是读还是写
,以第一个标志位,代表是否创建文件
可以传多个,还快
if(O_WRONLY&flag)
判断结果,所以O_WRONLY O_rdnly O_CREAT
这些都是只有一个比特位为1的数据,而且都不重复
#define O_WRONLY 0x1
#define O_RDONLY 0x2
等等
如果想要得到两个以上的功能,我们直接|就可以了
这就是通过比特位的方式传多组标记的做法
文件关闭
int fd=open("./lg.txt",O_WRONLY|O_CREAT);//以只写方式打开,如果文件不存在,就会帮助我们创建一个 //相当于C语言中的w选项, if(fd<0) { //打开文件失败 printf("open err\n"); } close(fd);//文件就关掉了
我们没有输入第三个参数(权限参数),那么加入不存在这个文件,那么产生的文件的权限是乱的
文件描述符
int fd=open("./g.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位 //以只写方式打开,如果文件不存在,就会帮助我们创建一个 //相当于C语言中的w选项,0644以二进制的方式显示权限 if(fd<0) { //打开文件失败 printf("open err\n"); } printf("fd:%d\n",fd); close(fd);//文件就关掉了
int fd=open("./g.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位 int fd1=open("./g1.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位 int fd2=open("./g2.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位 int fd3=open("./g3.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位 //以只写方式打开,如果文件不存在,就会帮助我们创建一个
我们发现是从3开始连续
文件描述符那么0,1,2去那里了呢
0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器
0 1 2 3 4 5 6 7
我们可以联想到一个数组的下标
open的返回值是OS系统给我们的
文件与进程
进程对文件的操作
要操作文件必须先打开文件
打开文件的本质:将文件相关的属性信息加载到内存
系统中会存在大量的进程,进程可以打开多个文件,系统中存在更多的打开的文件
那么OS 要把打开的文件管理起来,
(先描述再组织)
如果一个文件没有被打开,没有被创建,那么这个文件就在磁盘上,同理一个进程没有被打开,这个进程也在磁盘上面
如果创建了空文件(内容),要不要占磁盘空间呢,但是还有文件的属性,也是数据,,所以也是要占据磁盘空间,
磁盘文件=文件内容+文件的属性
对文件的操作:
对文件内容进行操作
对文件属性进行操作
管理的思路
先描述再组织
struct file { //包含了打开文件的相关属性信息 //链接属性 }
在进程里面有
struct task_struct
{
struct files_struct* fs;//地址,指向的就是其对应的内容
}
struct files_strtuct { struct file* fd_array[];//指针数组 }
因为array是一个指针数组,所以可以用对应的下标找到对应的地址0,1,2
相当于fd_array[0]就指向了一个文件
所以0,1,2分别被标准输入,标准输出,标准错误文件给占用
每次生成一个文件,再内存里面就要形成一个struct file结构,在把地址填入到array下标处
而我们再write和read的时候,都要传入fd
执行write和read调用的是进程,而进程就能通过自己的PCB 找到对应的fs指针,找到files_struct里面根据文件描述符找到对应的文件,进行相关操作
fd
本质就是内核中进程和文件关联的数组的下标
一切皆文件
一切皆为文件
每一个硬件都有其对应的write和read的方法
虚拟文件,可以类比于多态,使用函数指针,就用指针调用对应的函数,这些函数调用这些硬件对应的方法,
多态就是实现一切皆()的高级方法
如我们在上层调用read/write的时候,就指向了对应的fd,再指向其对应的方法
void Fd_Dewrite() { const char* msg="hello"; write(1,msg,strlen(msg));//向标准输出去写入 write(1,msg,strlen(msg));//向标准输出去写入 write(1,msg,strlen(msg));//向标准输出去写入 }
我们直接向标准输出去书出
直接从键盘输入
read(0,buf,sizeof(buf)-1);//直接从键盘上写入 printf("echo :%s",buf);
文件描述符的分配规则
void Fd_Alloc_Base() { close(0); close(2);//把0对应的标准输入给关闭 int fd=open("./g.txt",O_CREAT|O_WRONLY,0644); printf("fd=%d \n",fd); close(fd); }
分配规则
每次给新文件分配的fd,是从fd_array中找到最小的,没有被使用的,作为新的fd
void Fd_Alloc_Base() { close(1); close(2);//把0对应的标准输入给关闭 int fd=open("./g.txt",O_CREAT|O_WRONLY,0644); printf("fd=%d \n",fd); close(fd); }
我们把1的标准输出给关了,
没有显示到显示屏中,而是显示到了文件中
这就是输出重定向
原因:我们把1的文件描述符给关了,所以现在给g.txt里面的fd就是1,
而printf里面对应的文件的fd一定是1,所以现在g.txt的fd为=1,就写到了g.txt
而printf和fprintf里面的都是有相关的fd,因为操作系统最大,
write
功能:向文件描述符写入,在buf的用户缓冲区里面,期望写入cout个字节
return:返回的是实际向文件里面写入多少个字节的内容
#include<stdio.h> #include<sys/stat.h> #include<sys/types.h> #include<fcntl.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main() { int fd=open("demo.txt",O_CREAT|O_WRONLY,0644); if(fd<0) { perror("open"); exit(-1); } const char*msg="hello \n"; int cnt=5; while(cnt) { write(fd,msg,strlen(msg));//我们写入文件的过程中,我们要不要加入\0呢,不需要, //因为\0作为字符串的结束标志位,只是c的规定,而文件关心字符串的内容, cnt--; } close(fd); return 0; }
我们用write写入了5个hello,
read
功能:从文件描述符中读取指定内容,一次读取到的内容,都放到用户层缓冲区中,每次读取count个字节
return:如果count是我们期望读多少个字节,返回就是我们实际读取多少个字节
void FdRead() { int fd=open("./demo.txt",O_RDONLY);//以读的方式打开不涉及到创建,权限也不要写了 if(fd<0) { perror("open"); exit(-1); } char buff[1024]; ssize_t s=read(fd,buff,sizeof(buff)-1);//-1是因为我们不需要\0 if(s>0) { //说明我们读取到了有效的内容 //因为我们要把读取到的内容当作一个字符串看待,所以要在最结尾添加一个\0,作为字符串结束标志 buff[s]=0; printf("%s\n",buff); } close(fd); }
重定向
输出重定向
echo "hello world">log.txt
我们可以理解为把echo的1关掉,在把log打开,再把所有内容打印到里面
追加重定向
void Fd_Alloc_Base() { close(1); // close(2);//把0对应的标准输入给关闭 int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644); printf("fd=%d \n",fd); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); // close(fd); }
只在open的时候把append选项加上去
输入重定向
就是把原来从键盘里面读入的东西,现在从一个文件里面读
void Fd_Redefout() { close(0);//把输入给关掉 int fd=open("./g.txt",O_RDONLY); char line[128]; while(fgets(line,sizeof(line)-1,stdin))//因为现在g.txt的fd为0,所以stdin就是g.txt { //原本应该从键盘读取的内容,现在是从文件里面读取了 //输入重定向 printf("%s\n",line); } }
验证文件描述符
void Verify_IO() { printf("stdin-> %d",stdin->_fileno); printf("stdout-> %d",stdout->_fileno); printf("stderr-> %d",stderr->_fileno); }
dup2
newfd是oldfd的一份拷贝,数组内容的拷贝,指针的拷贝
所以全部变成old,
输出重定向
dup2(fd,1)
shell中的重定向
echo “hello” > file.c
fork->child->dup2(fd,1)->exec("echo,“echo”)
fork创建之后 子进程也有fd,而且文件描述符和父进程都一样
但是打开的那些文件不会新建,因为我们是在创建进程,
如果父进程打开了标准输入,输出,错误,子进程也会继承下去
因为bash是所有进程的父进程,而bash打开了标准输入,输出,错误,所以所有的子进程也都继承下去了
缓冲区
标准输出和标准错误
int main() { const char* msg="hello stdout\n"; write(1,msg,strlen(msg)); const char* msg2="hello stderr\n"; write(2,msg2,strlen(msg2)); return 0; }
我们发现只有标准输出重定向到了文件里面,但是标准错误仍然打印在屏幕里面了
因为重定向只有fd=1的被弄进去,而fd=2不会被弄进去
./redir >log.txt 2>&1
把标准输出和标准错误都重定向进去
$ cat log.txt hello stdout hello stderr
./redir >log.txt 2>&1
先执行前面第一条语句,此时把fd=1原本指向显示器改为指向一个特定文件,而fd=2的文件原本指向了显示屏,把1里面的内容拷贝到2里面,所以2也指向1指向的文件
缓存区
1.
int main() { const char* msg="hello stdout\n"; write(1,msg,strlen(msg)); const char* msg2="hello stderr\n"; write(2,msg2,strlen(msg2)); printf("hello printf\n"); fprintf(stdout,"hello fprintf\n"); close(1); return 0; }
第一次可以显示出fprintf
close后,第二次不能输出这些内容
2.
void Fd_Alloc_Base() { close(1); // close(2);//把0对应的标准输入给关闭 int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644); printf("fd=%d \n",fd); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); // close(fd); }
把fd关闭之后就没有显示内容了
c语言本身也
我们曾经说的缓冲区都是用户级缓冲区,都是语言层面
printf是向stdout写入(FILE*)–》(struct file)
我们使用printf和fprintf,我们并没有写到OS里面,而是写到c语言缓冲区,把c语言的缓存区刷新到操作系统,
我们在特定的情况下才会把数据刷新到内核缓冲区,
遇到\n的时候,会刷新到显示器上面,
进程退出的时候,会刷新FILE 内部的数据到OS,没有进程退出的时候数据还会在C缓冲区里面,
用户------>OS
刷新策略
立即刷新(不缓冲)
行刷新(行缓冲\n):如显示器打印,
全缓冲区满了才刷新(也就是不会溢出),比如,往磁盘文件里面写入
OS—> 硬件,也是同样使用的
如果发生了重定向,
显示器 --> log.txt
原本是行刷新(行缓冲),现在就变成了全缓冲
void Fd_Alloc_Base() { close(1); // close(2);//把0对应的标准输入给关闭 int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644); printf("fd=%d \n",fd); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); printf("hello world"); // close(fd); }
对于这个代码
如果不close的话,所有的信息都打印到了文件当中
没有close的话,所有的消息都直接输出到了c语言缓冲区里面,然后因为没有close掉fd,这批数据就会在进程退出的时候,数据就会刷新到内核,
把close放开,文件里面什么都没有,
所有的消息,刷新变成了全缓冲,有可能并没有被写满,说明可能没有立即被刷新到文件里头,而进程退出之前调用了close,就把文件描述符给关了,进程退出的时候数据还在缓冲区里面,来不及刷新到内核当中,所以文件里面就没有看到对应的内容
如果把close(1)关掉,那么printf把数据都刷新到用户缓冲区里头,行刷新,立马刷新了
如果在close之前
fflush(stdout),强制刷新缓冲区里面,就可以了写到文件里面了,
FILE*里面是有包含缓冲区的
int main() { const char *msg = "hello stdout\n"; write(1, msg, strlen(msg)); const char *msg2 = "hello stderr\n"; write(2, msg2, strlen(msg2)); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); close(1); return 0; }
我们发现我们即使close(1),仍然能够打印在显示屏上,因为三行刷新,有\n就刷新了
我们把内容重定向到文件里面,
发现只有write的1被写进去了
这是因为我们close(1),当重定向的时候原本要写到1里面的内容,显示到文件中,就从行刷新,变成了全缓冲,不立即刷新,而write是系统调用没有经过c语言缓冲区,就可以打印到文件里面,
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { const char *msg = "hello stdout\n"; write(1, msg, strlen(msg)); // const char *msg2 = "hello stderr\n"; // write(2, msg2, strlen(msg2)); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); fputs("hello puts\n",stdout); // close(1); fork(); return 0; }
我们如果2往显示器上打印,大家都正常,如果往文件里面打印,c接口会重复,系统接口不受影响
刷新策略变了,
写入文件里面的时候,就变成了全缓冲,数据就先写到了c语言缓冲区里面(不是操作系统提供的),
fork之后发生了写时拷贝,父进程写到了缓冲区里面,子进程也刷新到了缓冲区里面,当副进程退出的时候,就把数据i刷新出去,子进程也要刷新,所以因为写实拷贝的问题,出现了重复刷新
而如果在fork之前就把数据全部fflush出去的话,fork之后就不会发生写实拷贝,因为缓冲区里面没有数据了,可是write没有打印两个,