[Linux] 基础IO

作者华丞臧.
​​​​专栏:【LINUX】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 LeetCode刷题网站


文章目录

  • 一、文件I/O
    • 1.1 文件描述符fd
    • 1.2 重定向
      • dup2系统调用
    • 1.3 接口介绍
      • open
      • close
      • read
      • write
      • lseek
      • 接口实验
    • 1.4 缓冲区
      • 什么是缓冲区?
      • 为什么要有缓冲区?
      • 缓冲区的刷新策略
      • 缓冲区在哪里?
    • 1.5 模拟实现IO函数


一、文件I/O

C语言当中fopen和fclose通过文件的路径来打开和关闭文件,fread可以读取文件的内容,fwrite可以向文件中写入数据,在C语言当中还有各种入fputc、fputs、fget等用于读写文件的函数。

操作文件出了上述C接口,我们还可以使用系统调用接口来进行文件访问。

1.1 文件描述符fd

  • 文件存放在磁盘上的,也是数据,即便创建一个空文件也要占据磁盘空间;磁盘上的文件等于文件的内容加上文件的属性,即使内容为空描述该文件的属性也是不为空的。
  • 那么操作文件是在做什么呢?文件包括两个部分文件内容、文件属性,那么对文件的操作无疑就是对内容或者属性的操作。
  • 冯诺依曼体系结构规定,CPU只能从内存中读取数据,所以打开文件实质上是将磁盘中的文件加载到内存当中,并不是所有文件都是打开状态,而是需要使用时再打开。

[Linux] 基础IO_第1张图片

open是一个系统调用接口,其原型如下:

int open(const char *pathname, int flags, mode_t mode);

C语言中fopen打开文件后返回的是一个文件指针,而这里返回的是一个整型,这个整型代表什么呢?

这个整型就是文件描述符,首先我们要知道在内存中的一个进程中可以打开多个文件,当一个程序运行时会默认打开三个输出流分别是stdin、stdout、stderr,并且在Linux操作系统下一切皆文件,底层的一些硬件对于操作系统而言都是文件,操作系统启动肯定会打开某一些文件如屏幕、磁盘等。

  • 对于加载到内存中的文件,操作系统需要管理他们,因此需要先描述内存中的文件,再组织这些描述文件的数据结构,实现对文件的管理。
  • 而在Linux操作系统中使用结构体来描述文件,结构体中描述了文件的各种属性,然后再将这些结构体的地址用一个数组组织起来管理,这也就是为什么文件描述符是整型,它相当于是数组的下标。
  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。0、1、2对应的物理设备一般是:键盘,显示器,显示器。
  • 每个进程的PCB都有一个指向file结构体数组的指针,指向自己打开的file结构体数组。

[Linux] 基础IO_第2张图片
文件描述符分配规则:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。一般默认从3开始,因为0、1、2通常默认是标准输入stdin, 标准输出stdout, 标准错误stderr。

1.2 重定向

#include 
#include 
#include 
#include 
#include 

int main()
{
 	close(1);
 	int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
 	if(fd < 0){
 		perror("open");
 		return 1;
 	}
 	printf("fd: %d\n", fd);
 	fflush(stdout);
 
 	close(fd);
 	exit(0);
}

[Linux] 基础IO_第3张图片
此时,我们发现本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, < 。
重定向本质就是改变file结构体数组中文件描述符位置上指针的指向。
[Linux] 基础IO_第4张图片

dup2系统调用

函数原型如下:

#include 
//将oldfd重定向到newfd中
int dup2(int oldfd, int newfd);

oldfd:源文件描述符
newfd:目的文件描述符
返回值:
	成功,返回newfd
	失败,返回-1

使用方式如下:

#include 
#include 
#include 

int main() 
{
 	int fd = open("./log", O_CREAT | O_RDWR);
 	if (fd < 0) {
 		perror("open");
 		return 1;
	}
 	close(1);
 	dup2(fd, 1);
 	for (;;) 
 	{
 		char buf[1024] = {0};
 		ssize_t read_size = read(0, buf, sizeof(buf) - 1);
 		if (read_size < 0) {
 			perror("read");
 			break;
 		}
 		printf("%s", buf);
 		fflush(stdout);
 	}
 	return 0;
}

上述代码的功能就是将log文件重定向到文件描述符1的位置上,重定向完成后fd:1表示log文件的地址。
[Linux] 基础IO_第5张图片

1.3 接口介绍

open

#include 
#include 
#include 
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:这些参数都是宏,系统传递标记为是用位图结构来进行传递的。
 	O_RDONLY: 只读打开
 	O_WRONLY: 只写打开
 	O_RDWR  : 读,写打开
 			  这三个常量,必须指定一个且只能指定一个
 	O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 	O_APPEND: 追加写
 返回值:
 	成功:新打开的文件描述符
 	失败:-1

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

系统调用库函数

  • fopen、fclose、fread、fwrite都是C标准库当中的函数,称之为库函数。
  • 而open、close、read、write、lseek都是系统提供的接口,称之为系统调用接口。
  • C语言当中关于文件读写的库函数都是对系统调用接口的封装,方便二次开发。

close

#include 
//关闭文件描述符对应的文件
int close(int fd);

返回值:
	成功,返回0
	失败,返回-1

read

#include 

ssize_t read(int fd, void *buf, size_t count);

fd:文件描述符
buf:存放读取数据的空间
count:读取数据字节数

返回值:
	成功,返回读取到的字节数
	失败,返回-1

write

#include 

ssize_t write(int fd, const void *buf, size_t count);

fd:文件描述符
buf:需要写入数据的地址
count:写入数据的字节数

返回值:
	成功,返回写入数据的字节数
	失败,返回-1

lseek

#include 
#include 

off_t lseek(int fd, off_t offset, int whence);


fd:文件描述符
offset:偏移量	
whence:
	SEEK_SET:偏移量设置为偏移字节。
	SEEK_CUR:偏移量被设置为其当前位置加上偏移字节。
	SEEK_END:偏移量设置为文件大小加上偏移字节。

返回值:
	成功,返回从文件开始的以字节为单位的结果偏移位置
	失败,返回-1

接口实验

#include 
#include 
#include 
#include 
#include 

int main() 
{
    umask(0);
 	int fd = open("myfile", O_CREAT | O_RDWR | O_TRUC, 0666); //O_TRUC表示打开时清空文件内容
 	if (fd < 0) {
 	    perror("open");
 	    return 1;
	}

 	char buf[] = "hello world!";
 	ssize_t read_size = read(fd, buf, sizeof(buf));
 	if (read_size < 0) {
 		perror("read");
 	}

    int write_size = write(fd, buf, sizeof(buf));
 	if(write_size < 0)
    {
        perror("write");
    }
    return 0;
}

在运行上述代码后,可以看到在当前目录下创建了一个myfile的文件,打开该文件其内容如下图:
[Linux] 基础IO_第6张图片

1.4 缓冲区

什么是缓冲区?

  • 缓冲区本质就是一段内存。

为什么要有缓冲区?

  • 解放使用缓冲区进程的时间,进程不用等待传输数据的时间。
  • 缓冲区的存在可以集中处理数据刷新,减少IO的次数,可以提高整机的效率。
  • 缓冲区类似快递驿站,再快递到来之前我们不需要在驿站等待快递,可以去做其他的事情;快递到达驿站后,我们接收到通知去取快递。

缓冲区的刷新策略

  • 常规策略

    • 无缓冲(立即刷新)
    • 行缓冲(逐行刷新)如:显示器
    • 全缓冲(缓冲区满时刷新)如:块设备对应的文件,磁盘文件
  • 特殊策略

    • 进程退出
    • 用户强制刷新

缓冲区在哪里?

首先来看下面这段代码,其运行结果是什么呢?按照代码执行的顺序应该是先打印出 “printf” 再打印 “write” ,真的是这样吗?

#include 
#include 

int main()
{
    printf("printf"); //printf("printf\n");
    write(1, "write", 5);
    sleep(5);
    //close(stdout->_fileno); //关闭标准输出,stdout是C语言封装的文件指针类型,_fileno表示该文件指针对应的文件描述符
    return 0;
}

在这里插入图片描述
可以看到结果并不是顺序打印,printf 是封装了 write 系统调用的函数,上述结果也说明了printf函数首先会将数据写入缓冲区中,当数据积累到一定程度才会刷新缓冲区,write会将数据写入文件中。

那么上述缓冲区在哪里呢?修改上述代码在休眠五秒后关闭标准输出,再运行程序结果如下图:
在这里插入图片描述
当我们将标准输出关闭时,看到程序最后并没有打印出 “printf”,这说明printf并没有将数据写入内核级的缓冲区中,而是将数据写入C语言提供的语言级缓冲区中,而write是直接写入到文件中。再来看看下面的这段代码:

#include 
#include 
#include 

int main()
{
    char *str1 = "hello printf\n";
    char *str2 = "hello fprintf\n";
    char *str3 = "hello fputs\n";
    char *str4 = "hello write\n";

    printf(str1);
    fprintf(stdout, str2);
    fputs(str3, stdout);

    write(stdout->_fileno, str4, strlen(str4));

    fork();
}

[Linux] 基础IO_第7张图片
结果原理:

  • 第一次运行test程序时,是将数据写入到stdout即显示器文件中,显示器属于行缓冲因此会每一行每一行刷新,而我们写入的数据中结尾都带有\n,所以会立即刷新,当fork创建子进程时缓冲区数据已经刷新到显示器上了。
  • 第二次运行test程序并重定向到log.txt文件时,是将数据写入磁盘文件属于全缓冲,C接口缓冲区是自己的FILE内部维护的,属于父进程的数据区域,因此fork创建子进程会写时拷贝父进程的数据,所以父子进程都会刷新一次,write是直接将数据写入到文件中,fork时数据已经写入到文件中去了。

1.5 模拟实现IO函数

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024 

#define NONE_FLUSH 0x0
#define LINE_FLUSH 0x1
#define FULL_FLUSH 0x2

typedef struct MyFile
{
    int _fileno;
    char _buffer[NUM];
    int _end;
    int _flags;
}MyFile;

MyFile *my_fopen(const char *filename, const char *method)
{
    assert(filename);
    assert(method);
    int flags = O_RDONLY; 

    if(strcmp(method, "r") == 0)
    {
        flags = O_RDONLY;
    }
    else if(strcmp(method, "w") == 0)
    {
        flags = O_WRONLY | O_CREAT | O_TRUNC;
    }
    else if(strcmp(method, "r+") == 0)
    {
        flags = O_RDWR | O_CREAT;
    }
    else if(strcmp(method, "w+") == 0)
    {
        flags = O_RDWR | O_CREAT;
    }
    else if(strcmp(method, "a") == 0)
    {
        flags = O_WRONLY | O_CREAT | O_APPEND;
    }
    else if(strcmp(method, "a+") == 0)
    {
        flags = O_RDWR | O_CREAT | O_APPEND;
    }
    umask(0);
    int fileno = open(filename, flags, 0666);
    MyFile *fp = (MyFile*)malloc(sizeof(MyFile));
    if(fp == NULL) 
    {
        perror("malloc file\n");
        return NULL;
    }
    memset(fp, 0, sizeof(MyFile));

    fp->_fileno = fileno;
    if(fp->_fileno < 0) return NULL;
    fp->_flags |= LINE_FLUSH;
    fp->_end = 0;

    return fp;
}

void my_fwrite(MyFile *fp, const char * start, int size)
{
    assert(fp);
    assert(start);
    assert(size > 0);

    strncpy(fp->_buffer + fp->_end, start, size); //将数据写到缓冲区
    fp->_end += size;

    if(fp->_flags & NONE_FLUSH)
    {}
    else if(fp->_flags & LINE_FLUSH)
    {
        if(fp->_end > 0 && fp->_buffer[fp->_end - 1] == '\n')
        {
            write(fp->_fileno, fp->_buffer, fp->_end);
            fp->_end = 0;
        }
    }
    else if(fp->_flags & FULL_FLUSH)
    {}
}

void my_fflush(MyFile *fp)
{
    assert(fp);

    if(fp->_end > 0)
    {
        write(fp->_fileno, fp->_buffer, fp->_end);
        fp->_end = 0;
    }
}

void my_fclose(MyFile *fp)
{
    my_fflush(fp);
    close(fp->_fileno);
    free(fp);
}


int main()
{
    MyFile *fp = my_fopen("log.txt", "w");
    if(fp == NULL)
    {
        printf("my_fopen error\n");
        return 1;
    }

    const char *s = "hello myfile\n";
    my_fwrite(fp, s, strlen(s));
    printf("\n");
    sleep(3);

    const char *ss = "hello myfile";
    my_fwrite(fp, ss, strlen(ss));
    sleep(3);

    printf("写入了一个不满足条件的字符串\n");

    my_fclose(fp);
    return 0;
}

[Linux] 基础IO_第8张图片

你可能感兴趣的:(LINUX,linux)