【Linux系统编程】12. 基础IO(下)

文章目录

一、再谈 "重定向"

我们都知道C默认存在着3个输入输出流,标准输出流和标准错误流都是输出到显示器上,那么为什么要多出一个标准错误流呢?

  • 因为通过重定向,我们可以将常规消息和错误信息进行分离!

示例:

cpp 复制代码
// test1.cc
#include<iostream>
#include<cstdio>

int main()
{
    // 向标准输出进行打印, stdout,cin->1 
    std::cout << "hello cout" << std::endl; 
    printf("hello printf\n");
    
    // 向标准错误进行打印, stderr, cerr->2
    std::cerr << "hello cerr" << std::endl;
    fprintf(stderr, "hello stderr\n");
        
    return 0;
}

可以看到通过重定向,将标准输出流(stdout)的内容写入了log.txt文件之中,而剩下的标准错误流(stderr)的内容则正常输出到显示器上。

如果想将 stdout 和 stderr 的内容打印到同一个文件呢?

powershell 复制代码
./test1 1>log.txt  2>&1

二、Linux下"⼀切皆⽂件"

⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样的硬件设备,在Linux中也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;

这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写PIPE)的操作都可以⽤ write 函数来进⾏。

当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在/usr/src/kernels/3.10.0-957.21.3.el7.x86_64/include/linux/fs.h下,以下展示了该结构部分我们所关心的内容:

cpp 复制代码
struct file {
	...	 
	 
	struct inode *f_inode; 
	const struct file_operations *f_op;
	 
	...
	 
	atomic_long_t f_count; // 表示打开文件的引用计数,如果有多个文件指针指向它,就会增加f_count的值。 
	unsigned int f_flags; // 表示打开文件的权限 
	fmode_t f_mode; // 设置对文件的访问模式,例如:只读,只写等。 
	loff_t f_pos; // 表示当前读写文件的位置 
	 
	...	 
}__attribute__((aligned(4))); 

值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构也在fs.h下:

cpp 复制代码
struct file_operations {
	// 指向拥有该模块的指针
	struct module* owner;
	// 改变文件中的当前读/写位置, 并且新位置作为返回值
	loff_t(*llseek) (struct file*, loff_t, int);
	// 从设备中获取数据,返回值代表成功读取的字节数
	ssize_t(*read) (struct file*, char __user*, size_t, loff_t*);
	// 发送数据给设备,返回值代表成功写的字节数
	ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*);
	// 初始化⼀个异步读 -- 可能在函数返回前不结束的读操作
	ssize_t(*aio_read) (struct kiocb*, const struct iovec*, unsigned long, loff_t);
	// 初始化设备上的⼀个异步写
	ssize_t(*aio_write) (struct kiocb*, const struct iovec*, unsigned long, loff_t);
	// 对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用 
	int (*readdir) (struct file*, void*, filldir_t);

	unsigned int (*poll) (struct file*, struct poll_table_struct*);
	int (*ioctl) (struct inode*, struct file*, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file*, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file*, unsigned int, unsigned long);
	
	// 将设备内存映射到进程的地址空间
	int (*mmap) (struct file*, struct vm_area_struct*);
	// 打开一个文件 
	int (*open) (struct inode*, struct file*);
	// 在进程关闭它的设备文件描述符的拷贝时调用 
	int (*flush) (struct file*, fl_owner_t id);
	// 在文件结构被释放时引用这个操作
	int (*release) (struct inode*, struct file*);
	// 用户调用来刷新任何挂着的数据
	int (*fsync) (struct file*, struct dentry*, int datasync);

	int (*aio_fsync) (struct kiocb*, int datasync);
	int (*fasync) (int, struct file*, int);
	
	// 实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它
	int (*lock) (struct file*, int, struct file_lock*);

	ssize_t(*sendpage) (struct file*, struct page*, int, size_t, loff_t*, int);
	unsigned long (*get_unmapped_area)(struct file*, unsigned long, unsigned
		long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file*, int, struct file_lock*);
	ssize_t(*splice_write)(struct pipe_inode_info*, struct file*, loff_t*,
		size_t, unsigned int);
	ssize_t(*splice_read)(struct file*, loff_t*, struct pipe_inode_info*,
		size_t, unsigned int);
	int (*setlease)(struct file*, long, struct file_lock**);
};

file_operations 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operations 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。

⼀张图总结:

上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct filefile_operations 中的各种函数回调,让我们开发者只⽤ file 便可调取 Linux 系统中绝⼤部分的资源!!这便是"linux下⼀切皆⽂件"的核⼼理解。

三、缓冲区

1、什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。

2、为什么要引⼊缓冲区机制

读写⽂件时,如果不开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤户空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应⽤缓冲区可⼤大提⾼计算机的运⾏速度

⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

3、缓冲类型

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式。

  • ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符(\n)时,标准I/O库函数将会执⾏系统调⽤操作。涉及显示器(标准输入或标准输出)的操作时,使⽤⾏缓冲⽅式。

  • ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新⽅式,执行flush语句也会引发缓冲区的刷新。

具体的刷新过程如下图:

示例:

cpp 复制代码
#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<string.h>    
    
int main()    
{    
    close(1); // fd == 1    
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);    
    if(fd < 0)     
    {    
        perror("open log.txt");                                                                                
        return 1;                       
    }                                                                                                                           
    printf("fd: %d\n", fd);                                      
    printf("hello world\n");                                     
    close(fd);                                                   
                                                                 
    return 0;                                                    
}

我们本来想使⽤输出重定向,让内容输出到"log.txt"⽂件中,但我们发现,程序运⾏结束后,内容并没有被写入。

这是由于我们重定向将1号描述符指向磁盘⽂件,缓冲区的刷新⽅式成为了全缓冲。⽽我们写⼊的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘⽂件中。

怎么办呢?可以使⽤fflush强制刷新下缓冲区。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main()
{
    close(1); // fd == 1
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if(fd < 0) 
    {
        perror("open log.txt");
        return 1;
    }    
    printf("fd: %d\n", fd);
    printf("hello world\n");
    fflush(stdout);
    close(fd);
    
    return 0;
}

还有⼀种解决⽅法,刚好可以验证⼀下stderr是不带缓冲区的:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main()
{
    close(2); // fd == 2
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
    if(fd < 0) 
    {
        perror("open log.txt");
        return 1;
    }    
    printf("fd: %d\n", fd);
    printf("hello world\n");
    close(fd);
    
    return 0;
}

通过重定向将2号⽂件描述符指向磁盘⽂件,由于stderr没有缓冲区,"hello world"不⽤fflush强制刷新就可以写⼊⽂件。

4、FILE

  • 因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通过fd访问的。
  • 所以C库当中的FILE结构体内部,必定封装了fd。

示例:

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
    const char* msg0 = "hello printf\n";
    const char* msg1 = "hello fwrite\n";
    const char* msg2 = "hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();

    return 0;
}

正常执行程序:

但如果对进程实现输出重定向呢?

我们发现 printf 和 fwrite(库函数)都输出了2次,⽽ write(系统调⽤)只输出了⼀次。为什么呢?肯定和fork有关!

  • ⼀般C库函数写⼊⽂件时是全缓冲,⽽写⼊显示器是⾏缓冲。

  • printf fwrite 库函数会⾃带缓冲区,当发⽣重定向到普通⽂件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。

  • ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后。

  • 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。

  • 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。

  • write 没有变化,说明没有所谓的缓冲区。

综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这⾥所说的缓冲区,都是⽤户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的"上层",是对系统调⽤的"封装",但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

四、实现简易版libc库

Makefile

powershell 复制代码
code:mystdio.c usercode.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -rf code

mystdio.h

cpp 复制代码
#pragma once 

#include<stdio.h>

#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)

typedef struct IO_FILE
{
    int fileno;
    int flag;
    char outbuffer[MAX];
    int bufferlen;
    int flush_method;
}MyFile;

MyFile* MyFopen(const char* path, const char* mode);
void MyFclose(MyFile*);
int MyFwrite(MyFile*, void* str, int len);
void MyFFlush(MyFile*);

mystdio.c

cpp 复制代码
#include"mystdio.h"
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>

static MyFile* BuyFile(int fd, int flag)
{
    MyFile* f = (MyFile*)malloc(sizeof(MyFile));
    if(f == NULL) return NULL;
    f->bufferlen = 0;
    f->fileno = fd;
    f->flag = flag;
    f->flush_method = LINE_FLUSH;
    memset(f->outbuffer, 0, sizeof(f->outbuffer));
    return f;
}

MyFile* MyFopen(const char* path, const char* mode)
{
    int fd = -1;
    int flag = 0;
    if(strcmp(mode,"w") == 0)
    {
        flag = O_CREAT | O_WRONLY | O_TRUNC;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode,"a") == 0)
    {
        flag = O_CREAT | O_WRONLY | O_APPEND;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode,"r") == 0)
    {
        flag = O_RDWR;
        fd = open(path, flag);
    }
    else 
    {
        // ...
    }

    if(fd < 0) return NULL;
    return BuyFile(fd, flag);
}

void MyFclose(MyFile* file)
{
    if(file->fileno < 0) return;
    MyFFlush(file);
    close(file->fileno);
    free(file);
}

int MyFwrite(MyFile* file, void* str, int len)
{   
    // 1. 拷贝
    memcpy(file->outbuffer + file->bufferlen, str, len);
    file->bufferlen += len;

    // 2. 尝试判断是否满足刷新条件!
    if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] =='\n')
    {
        MyFFlush(file);
    }
    return 0;
}

void MyFFlush(MyFile* file)
{
    if(file->bufferlen <= 0) return;
    // 把数据从用户拷贝到内核文件缓冲区中
    int n = write(file->fileno, file->outbuffer, file->bufferlen);
    (void)n;
    fsync(file->fileno);
    file->bufferlen = 0;
}

usercode.c

cpp 复制代码
#include"mystdio.h"
#include<string.h>
#include<unistd.h>

int main()
{
    MyFile* filep = MyFopen("./log.txt","a");
    if(!filep)
    {
        printf("fopen error!\n");
        return 1;
    }

    int cnt = 10;
    while(cnt--)
    {
        char* msg = (char*)"hello myfile!";
        MyFwrite(filep, msg, strlen(msg));
        MyFFlush(filep);
        printf("buffer: %s\n", filep->outbuffer);
        sleep(1);
    }
    MyFclose(filep);

    return 0;
}

运行结果:

相关推荐
网络小白不怕黑2 小时前
Docker Compose与私有仓库
运维·docker·容器
Winter_Sun灬2 小时前
CentOS 7 编译安卓 arm64-v8a 版 OpenSSL 动态库(.so)
android·linux·centos
ベadvance courageouslyミ2 小时前
系统编程之进程
linux·进程·pcb结构体
松涛和鸣2 小时前
29、Linux进程核心概念与编程实战:fork/getpid全解析
linux·运维·服务器·网络·数据结构·哈希算法
代码不行的搬运工3 小时前
显式拥塞通知(ECN)机制
运维·服务器·网络·算力网络
BJ_Bonree3 小时前
Bonree ONE 发布直通车| 如何利用核心链路,快速排查定位系统故障?
大数据·运维·人工智能
南棱笑笑生3 小时前
20251211给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时适配adb【type-C0】
linux·c语言·adb·rockchip
科士威传动3 小时前
微型导轨的类型性能差异与场景适配需求
大数据·运维·人工智能·科技·机器人·自动化
Yengi3 小时前
【test】gtkmm-环境搭建
linux