【Linux】基础IO-下

时间:2024-10-29 07:07:51
#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.txt

echo "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);
}