linux_缓冲区及简单libc库【Ubuntu】

文章目录

一、理解"一切皆文件"

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西, 使用的接口跟文件接口也是一致的。

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

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

c 复制代码
struct file 
{

... 

struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

...

atomic_long_t f_count; // 表示打开文件的引用计数,如果有多个文件指针指向它,就会增加f_count的值。
unsigned int f_flags; 	// 表示打开文件的权限
fmode_t f_mode; 	// 设置对文件的访问模式,例如:只读,只写等。
					//所有	的标志在头文件<fcntl.h> 中定义		
loff_t 	f_pos; 	// 表示当前读写文件的位置

... 

} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

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

c 复制代码
struct file_operations { 
	struct module *owner;
	//指向拥有该模块的指针;
	loff_t (*llseek) (struct file *, loff_t, int);
	//llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	//用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个"signed size" 类型, 常常是目标平台本地的整数类型).
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	//发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
	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);
	//初始化设备上的一个异步写.
	int (*readdir) (struct file *, void *, filldir_t);
	//对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对**文件系统**有用.
	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 *);
	//mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
	int (*open) (struct inode *, struct file *);
	//打开一个文件
	int (*flush) (struct file *, fl_owner_t id);
	//flush 操作在进程关闭它的设备文件描述符的拷贝时调用;
	int (*release) (struct inode *, struct file *);
	//在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
	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 *);
	//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_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

介绍完相关代码,一张图总结:

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct filefile_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是"linux下一切皆文件"的核心理解。

二、缓冲区

1、什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

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

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

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

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

3、缓冲类型

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

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
  • 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;

示例如下:

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

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

我们本来想使用重定向思维,让本应该打印在显示器上的内容写到"log.txt"文件中,但我们发现,程序运行结束后,文件中并没有被写入内容:

bash 复制代码
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls 

log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt 
[hyb@VM-8-12-centos buffer]$ 

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。

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

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

还有一种解决方法,刚好可以验证一下stderr是不带缓冲区的,代码如下:

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

int main() 
{
	close(2);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0) 
	{
		perror("open");
		return 0;
	} 
	perror("hello world");
	close(fd);
	return 0;
}

这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,"hello world"不用fflush就可以写入文件:

bash 复制代码
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt 
hello world: Success

4、FILE

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

来段代码在研究一下:

c 复制代码
#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(msg1), 1, stdout);
	write(1, msg2, strlen(msg2));
	
	fork();
	
	return 0;
}

运行出结果:

bash 复制代码
hello printf
hello fwrite 
hello write 

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

bash 复制代码
hello write 
hello printf
hello fwrite
hello printf
hello fwrite 

我们发现 printffwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数+会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文
    件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

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

如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

c 复制代码
在/usr/include/libio.h
struct _IO_FILE { 
	int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

	//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	char* _IO_read_ptr; 	/* Current read pointer */
	char* _IO_read_end; 	/* End of get area. */
	char* _IO_read_base; 	/* Start of putback+get area. */
	char* _IO_write_base; 	/* Start of put area. */
	char* _IO_write_ptr; 	/* Current put pointer. */
	char* _IO_write_end; 	/* End of put area. */
	char* _IO_buf_base; 	/* Start of reserve area. */
	char* _IO_buf_end; 		/* End of reserve area. */
	/* The following fields are used to support backing up and undo. */
	char *_IO_save_base; 	/* Pointer to start of non-current get area. */
	char *_IO_backup_base; 	/* Pointer to first valid character of backup area */
	char *_IO_save_end; 	/* Pointer to end of non-current get area. */
	
	struct _IO_marker *_markers;
	struct _IO_FILE *_chain;
	
	int _fileno; //封装的⽂件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];
	
	/* char* _save_gptr; char* _save_egptr; */
	
	_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

5、简单设计一下libc库

my_stdio.h

c 复制代码
$ cat my_stdio.h
#pragma once

#define SIZE 1024

#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
	int flag; // 刷新⽅式
	int fileno; // ⽂件描述符
	char outbuffer[SIZE];
	int cap;
	int size;
	// TODO
};

typedef struct IO_FILE mFILE;

mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c

c 复制代码
$ cat my_stdio.c 
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
	int fd = -1;
	if(strcmp(mode, "r") == 0)
	{
		fd = open(filename, O_RDONLY);
	} 
	else if(strcmp(mode, "w")== 0)
	{
		fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
	} 
	else if(strcmp(mode, "a") == 0)
	{
		fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
	} 
	if(fd < 0) 
		return NULL;
	mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
	if(!mf)
	{
		close(fd);
		return NULL;
	} 
	
	mf->fileno = fd;
	mf->flag = FLUSH_LINE;
	mf->size = 0;
	mf->cap = SIZE;
	
	return mf;
} 
void mfflush(mFILE *stream)
{
	if(stream->size > 0)
	{
		// 写到内核⽂件的⽂件缓冲区中!
		write(stream->fileno, stream->outbuffer, stream->size);
		// 刷新到外设
		fsync(stream->fileno);
		stream->size = 0;
	}
} 
int mfwrite(const void *ptr, int num, mFILE *stream)
{
	// 1. 拷⻉
	memcpy(stream->outbuffer+stream->size, ptr, num);
	stream->size += num;
	
	// 2. 检测是否要刷新
	if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1]== '\n')
	{
		mfflush(stream);
	}
	return num;
} 
void mfclose(mFILE *stream)
{
	if(stream->size > 0)
	{
		mfflush(stream);
	} 
	close(stream->fileno);
}

main.c

c 复制代码
$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
	mFILE *fp = mfopen("./log.txt", "a");
	if(fp == NULL)
	{
		return 1;
	} 
	int cnt = 10;
	while(cnt)
	{
		printf("write %d\n", cnt);
		char buffer[64];
		snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
		cnt--;
		mfwrite(buffer, strlen(buffer), fp);
		mfflush(fp);
		sleep(1);
	} 
	mfclose(fp);
}
相关推荐
java_logo4 小时前
Docker 部署 Elasticsearch 全流程手册
大数据·运维·mongodb·elasticsearch·docker·容器
若尘拂风4 小时前
FreeSWITCH配置文件解析(11) 模块配置文件(modules.conf)
服务器·tcp/ip·udp·freeswitch
MeowKnight9584 小时前
【C】函数指针
c语言·1024程序员节
TG_yunshuguoji4 小时前
亚马逊云渠道商:AWS实例自动替换策略在哪里设置?
运维·服务器·云计算·aws
Stay Passion4 小时前
Docker 组件:Docker Swarm
运维·docker·容器
weixin_454372114 小时前
0.机顶盒晶晨s905l3b芯片--刷入第三方系统+安卓9 root教程+armbian写入EMMC教程
linux·学习方法
溜追4 小时前
OEC-Turbo刷群晖&Armbian流程记录
linux·经验分享·嵌入式硬件
胜天半月子5 小时前
嵌入式开发 | C语言 | 单精度浮点数4字节可以表示的范围计算过程
c语言·嵌入式c·1024程序员节·单精度浮点数计算
极客范儿5 小时前
新华三H3CNE网络工程师认证—STP状态机与收敛过程
服务器·网络·stp·1024程序员节