复习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指向的文件
缓存区
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后,第二次不能输出这些内容
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没有打印两个,