目录
1、重定向
2、缓冲区
3、简单模拟实现C文件标准库
1、重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
int fd = open(filename, O_CREAT|O_WRONLY|o_TRUNC,0666);
if(fd < 0)
{
perror("open"); //当打开出错时,就会显示open: 错误描述信息
return 1;
}
printf("fd:%d\n",fd);
//返回值是你实际写入到这个文件中的个数
int cnt = 5;
const char *msg = "hello Linux";
while(cnt)
{
write(fd, msg,strlen(msg));
cnt--;
}
close(fd);
return 0;
}
文件描述符的分配规则:从0下标开始寻找最小的没有被使用的数组位置,它的下标就是新文件的文件描述符。eg:flose(0),那么打开的新文件的fd就是0.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
close(1);//关闭显示器文件
int fd = open(filename, O_CREAT|O_WRONLY|o_TRUNC,0666);
if(fd < 0)
{
perror("open"); //当打开出错时,就会显示open: 错误描述信息
return 1;
}
int cnt = 5;
const char *msg = "hello Linux";
while(cnt)
{
write(1, msg,strlen(msg)); //向显示器写入
cnt--;
}
close(fd);
return 0;
}
结果:./myproc
(没有任何显示) 但是cat log.txt
显示:hello Linux
hello Linux
hello Linux
hello Linux
hello Linux
我们可以看到,需要向显示器打印的数据被打印到了文件里------这其实就是输出重定向
其实是因为:当flose(1)的时候,系统就会重新将1下标分配给log.txt,可是,在write的时候是向1写的,就会写入到 log.txt 文件(上图就是重定向的原理) 重定向其实是将数组的内容做修改。
数组的内容:就是文件的地址。 系统中有专门用作重定向的系统调用,本质就是将3号下标的log.txt file* 拷贝到1号下标,这样,就可以是log.txt分配的fd是1,这样写入的话就可以直接完成重定向。
#include <unistd.h>int dup2(int oldfd, int newfd);// oldfd 是最后两个都变成的fd(即你要往哪个里面写) newfd是需要将oldfd复制到的下标 其实是指针内容进行拷贝
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
int fd = open(filename, O_CREAT|O_WRONLY|o_TRUNC,0666);
//int fd = open(filename, O_CREAT|O_WRONLY|O_APPEND,0666);//追加重定向
if(fd < 0)
{
perror("open"); //当打开出错时,就会显示open: 错误描述信息
return 1;
}
int cnt = 5;
//重定向
dup2(fd,1); //直接使用系统调用来进行重定向
close(fd); //可以关也可以不关,这样就不会造成文件描述符浪费
const char *msg = "hello Linux";
while(cnt)
{
write(1, msg,strlen(msg)); //向显示器写入
cnt--;
}
close(fd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
int fd = open(filename,O_RDONLY);//输入重定向
if(fd < 0)
{
perror("open");
return 1;
}
//重定向 //dup2(int oldfd, int newfd); //可以理解为用fd替换0
dup2(fd, 0);
char inbuffer[1024];
ssize_t s = read(0, inbuffer, sizeof(inbuffer)-1);
//s保存的是实际读取的字节数
if(s > 0)
{
inbuffer[s] = '\0';//将第s个字节设置为\0 字符串结束
printf("echo# %s\n", inbuffer);
}
close(fd);
return 0;
}
//解释:本来是从键盘文件读的,但是写了输入重定向,就是直接从文件中读
运行的结果就直接将文件内容读出来了。//直接从文件里面读,就叫输入重定向
重定向的本质:其实就是对文件描述符表的内容的地址做修改
int fd = open(filename, O_CREAT|O_WRONLY|o_TRUNC,0666);
dup(fd,1);
printf("hello printf!\n");
fprintf(stdout,"hello fprintf\n");
//上面这两个函数是库函数,底层肯定有stdout
//int fprintf(FILE *stream, const char *format, ...);
命令行上:
echo "hello Linux" > log.txtecho "heoll Linux" >> log.txt //追加重定向
cat < log.txt
进程历史打开的文件与进行的各种重定向关系 都和未来进行程序替换无关! 程序替换并不影响文件访问!这也相当于形成了文件管理和进程管理的解耦!
stout && sterr 的区别
stdin->fd: 0 标准输入
stdout->fd: 1 标准输出 向显示器输出
stderr->fd: 2 标准错误 向显示器输出都是向显示器输出,那两个有上面区别?????
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stdout, "hello normal message\n");
fprintf(stderr, "hell error message\n");
fprintf(stderr, "hell error message\n");
fprintf(stderr, "hell error message\n");
fprintf(stderr, "hell error message\n");
return 0;
}
上面的代码:
直接运行:
但是,当我们进行重定向时: 我们会发现 error message 没有被重定向 只有normal message被重定向了 因为normal信息打印的时候用的是stdout 这是标准输出 而 stderr是标准错误 我们只是使用了 > 进行了输出重定向(这是1),也就是让1不要指向显示器,指向我创建的文件(本质是:指针数组存的地址被改变了)
error message 没有被重定向 只有normal message被重定向了 ' > ' 是输出重定向 就是让1不要指向显示器 而是 直接指向你创建的文件 也就是说,让本来应打印到标准输出的信息 打印到了文件里
./mytest 1> normal.log 2> err.log 这样写就可以将normal信息和error信息都重定向到文件中。1其实可以不写,因为 > 本来就是输出重定向,但是要想重定向err信息(即标准错误输出)就必须加2
也可以将两个一起重定向 ./mytest >all.log 2>&1 1已经做了前面的 &1的意思就是将1里面的内容写到2里面 即 1先指向新创的文件 然后让2 里面的地址 也变成和1一样的(即同一个文件的地址) 因此两个就指向了同一个文件.
这就是为什么有时候用printf有时候用perror
- 使用
printf
时,你主要是输出程序运行中的各种信息。- 使用
perror
时,你是在处理错误,并希望获取系统提供的错误描述。
2、缓冲区
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
//C语言提供的
printf("hello printf\n"); //stdout->1
fprintf(stdout, "hello fprintf\n"); //stdout->1
fwrite(fstr,strlen(fstr), 1, stdout); //stdout->1
//操作系统提供的系统调用接口
write(1, str, strlen(str)); //1
fork();
return 0;
}
补充:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite的返回值其实是nmemb的个数. 4字节 10个4字节 返回的是写入到基本单位的个数.
运行上面的代码,正常打印四句.但是./myfile > log.txt 即重定向之后:
通过实验现象 我们可以发现 C接口被打印了两次. 但是系统调用接口的打印只有一次,也就是说,系统调用不会受其他东西的影响. 那么出现这样的现象是为什么呢??
我们再看一个代码.
#include<stdio.h>
#include<string.h>
#include<unistd.h>
代码1 //
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
printf("hello printf\n"); //stdout->1
fprintf(stdout, "hello fprintf\n"); //stdout->1
fwrite(fstr,strlen(fstr), 1, stdout); //stdout->1
flose(1);
return 0;
}
// 代码2
int main()
{
const char *fstr = "hello fwrite";
const char *str = "hello write";
printf("hello printf"); //stdout->1
fprintf(stdout, "hello fprintf"); //stdout->1
fwrite(fstr,strlen(fstr), 1, stdout); //stdout->1
flose(1);
return 0;
}
/ 代码3
int main()
{
const char *str = "hello write";
write(1, str, strlen(fstr), 1, stdout);
flose(1);
return 0;
}
代码1 和 代码2 的区别就是1 有\n 而代码2 没有,那为什么有\n,就算close(1) 也可以打印出三条打印信息呢??? 为什么3 可以打印出hello write ??
printf/fprintf/fwrite/ fputs......---->C的库函数
他们的底层一定调用write(系统调用)
其实,调用printf fprintf fwrite 这些函数是,其实已经将这些字符串 写进缓冲区 里了,只不过这个缓冲区一定不在OS内. 因为,如果在系统级别的缓冲区,当close的时候,因为close也是系统级别的,一定会将缓冲区的内容刷新出来,但是我们并没有看到这样的现象,因此,可以说明这个缓冲区不是系统级别的缓冲区!!!!! 他们不是通过write将数据写到内核缓冲区的.
就像write是系统调用接口,它是直接写到系统缓冲区里面close的时候将数据刷出来. C语言其实会给我们提供一个缓冲区,这个缓冲区是用户级别的, 调用库函数就是将数据写入到用户级别的缓冲区里面,当到合适的时候,比如说:碰到了强制刷新 、close fclose文件描述符 、 或者字符串里面有\n 这样C库才会自动调用write将数据写到系统的缓冲区中,然后刷新出来.
但是,要刷的时候如果1号文件描述符被关闭了 就刷不出来了显示器的文件的刷新方式是行刷新,所以在printf执行完的时候,如果遇到\n 将数据立即进行刷新 eg:将上面的字符串都带上\n 然后close(1) 即使这样还是能打印出来 (因为在close(1) 之前,数据就已经被写到了操作系统里) 但是去掉 \n 就只有write系统调用才能打印出来.
总结一下:当close(1)之后,用户级别的缓冲区的数据就不会刷新到内核级别的缓冲区中了。刷新的本质就是将数据通过1 + write 写入到内核中.而且,目前我们认为,只要将数据刷新到内核,数据就可以到硬件了。操作系统会自动帮我们把数据写到磁盘/显示器。
缓冲区的刷新策略(用户级别的缓冲区):a、无缓冲:直接刷新 b、行缓冲:不刷新,直到碰到\n(显示器的) c、全缓冲:缓冲区满了才刷新 (文件写入) 根据缓冲区的刷新策略来决定什么时候调write()系统调用将数据写到缓冲区里。
补充问题:
1、进程退出的时候数据也会被刷新
2、为什么要有这个缓冲区(用户级别的)
a、解决效率问题---用户的效率问题
b、配合格式化
3、这个缓冲区在哪?
我们之前说过FILE里面肯定封装了fd,其实里面还有对应打开文件的缓冲区字段和维护信息fprintf(stdout,"hello world\n");
stdout是FILE*类型的,这个结构体里面{int fd = 1; 缓冲区} hello world就是先放到这个缓冲区里面的,等到合适的时候,调用write刷新到系统缓冲区。
这个FILE对象属于用户级别呢?还是操作系统级别?
语言都属于用户层
这个缓冲区,是不是属于用户级的缓冲区呢?
答案是:是的!FILE *fopen(const char *path, const char *mode) fopen的返回值为什么是FILE*
fopen是C标准库给我们提供的接口, 调用open在内核层建立内核级别的文件对象,并且拿到文件描述符;在语言层 malloc(FILE) 所以返回的就是FILE* 这个FILE里面就帮我们封装了文件描述符和缓冲区 也就是说,这个空间在C标准库里面已经帮我们做好了。
那么,我们再回头看一下之前的问题:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
//C语言提供的
printf("hello printf\n"); //stdout->1
fprintf(stdout, "hello fprintf\n"); //stdout->1
fwrite(fstr,strlen(fstr), 1, stdout); //stdout->1
//操作系统提供的系统调用接口
write(1, str, strlen(str)); //1
fork();
return 0;
}
重定向之后,由向显示器写入变成向文件写入。那么缓冲区的刷新策略也就由 行刷新变成全刷新。只有当缓冲区被写满的时候才进行刷新。 write是系统调用,所以hello write先被打印 而且他不会受其他东西的影响,所以只打印一次。fork()创建了子进程, 因为进程退出会刷新缓冲区,那么子进程也要对缓冲区的数据进行刷新,这也就相当于之前所说的对数据的写入,子进程就要将缓冲区需要刷新的数据给自己拷贝一份。 因此 子进程和父进程结束的时候都会刷出数据。
没有发生重定向时,是行刷新,在没有fork之前,这些数据就已经被刷到系统缓冲区,在调用fork之后,用户缓冲区是没有数据的。因此就只打印四句。
3、简单模拟实现C文件标准库
main.c
#include "Mystdio.h"
int main()
{
_FILE *fp = _fopen("test.txt", "w");
if(fp == NULL) return 1;
const char *msg = "hello world";
_fwrite(fp, msg, strlen(msg));
// _fclose(fp);
_fclose(fp);
return 0;
}
Mystdio.h
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include <string.h>
#define SIZE 1024
//刷新方式
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
//创建FILE结构体
typedef struct IO_FILE{
int fileno;
int flag;
//char inbuffer[SIZE];//输入缓冲区
//int in_pos;
char outbuffer[SIZE]; //输出缓冲区 //用一下这个
int out_pos;//代表输出缓冲区被使用了多少
//在系统看来这个buffer没有类型,由上层的printf、scanf自己去解释
}_FILE
_FILE *_fopen(const char*filename, const char *flag);//打开文件的名字 打开文件的模式
int _fwrite(_FILE *fp, const char *s, int len);
void _fclose(_FILE *fp);
#endif
Mystdio.c
#include "Mystdio.h"
#inckude <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#define FILE_MODE 0666 //权限
_FILE *_fopen(const char*filename, const char *flag) //文件名称 打开模式
{
//flag -> "w" "a" "r" 演示三种
assert(filename);
assert(flag);
int f = 0;
int fd = -1;
if(strcmp(flag, "w") == 0) {
f = (O_CREAT|O_WRONLY|O_TRUNC);
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "a") == 0) {
f = (O_CREAT|O_WRONLY|O_APPEND);
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "r") == 0) {
f = O_RDONLY;
fd = open(filename, f);
}
else
errno = 2; return NULL;
if(fd == -1) return NULL;//文件打开失败
//文件打开成功
//创建文件对象让别人用
_FILE *fp = (FILE*)malloc(sizeof(_FILE));
if(fp == nuLL) return NULL; //文件创建失败
fp->fileno = fd;
fp->flag = FLUSH_LINE;
fp->out_pos = 0;
return fp;
}
int _fwrite(_FILE *fp, const char *s, int len)
{
memcpy(&fp->outbuffer[fp->out_pos], s, len); //将s复制到输出缓冲区里面
fp->out_pos += len; //更新缓冲区
if(fp->flag & FLUSH_NOW) //立即刷新
{
write(fp->fileno, fp->outbuffer, len);//文件描述符 数据在哪里 要写入的字符的个数
fp->out_pos = 0;//缓冲区刷新完之后清空pos
}
else if(fp->flag & FLUSH_LINE)
{
if(fp->outbuffer[fp->out_pos - 1] == '\n') {
write(fp->fileno, fp->outbuffer, len);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL)
{
if(fp->out_pos ==SIAE){
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
return len;
}
_fflush(_FILE *fp)
{
if(fp->out_pos > 0){
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
void _fclose(_FILE *fp)
{
if(fp == NULL) return;
//在关闭文件之前,需要强制对缓冲区中的数据进行刷新
_fflush(fp);
close(fp->fileno);
free(fp);
}