Linux文件编程(系统API调用)

Linux文件编程

对于Linux的输入输出而言,它并不是直接输出到屏幕上或者直接从键盘中获取输入。这中间经过了一个缓存。

标注C的IO缓存类型

全缓存

  • 要求填满整个缓存区后才进行I/O系统调用操作。对于磁盘文件通常要求使用全缓存访问。

行缓存

  • 涉及一个终端时(例如标准输入和标准输出),使用行缓存。

    行缓存的特点:

    • 行缓存满自动输出
    • 碰到换行符自动输出

无缓存

  • 标准错误流stderr通常是不带缓存区的,这使得错误信息能够尽快的显示出来。

当写入的字节数不能够满足要填满的最大缓存区这一条件,可以调用fflush函数强制清空缓存,将缓存中的数据输出到指定的流中。

当程序结束之前会自动将缓存里边的内容全部清空。

代码示例–缓存区的存在
#include <stdio.h>

int main()
{
	printf("haha");

	while(1);

	return 0;
}

image-20240826174107738

上边的这行代码由于因为有缓存的缘故,所以当去调用printf函数去向屏幕输出东西不加换行的时候,没有任何东西输出,这就说明在代码和屏幕之间确实是存在一个缓存区。此时若想要输出,可以在printf函数后边加一个换行符或者使用fflush函数强制清空缓存

image-20240826174550752

image-20240826174610755

文件I/O系统调用

下列有关文件的函数都是内核提供的系统调用,它们都是不带缓存功能的。

标准C库关于文件的输入输出函数

char *fgets(char *restrict s, int n, FILE *restrict stream);
int fputs(const char *restrict s, FILE *restrict stream);
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ...);
size_t fread(void *restrict ptr, size_t size, size_t nitems,FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems,FILE *restrict stream);

int scanf(const char *restrict format, ...);
int printf(const char *restrict format, ...);

FILE结构体

typedef struct iobuf
{
    int cnt;		//剩余的字节数
    char *ptr;		//下一个字符的位置
    char *base;		//缓冲区的位置
    int flag;		//文件访问模式
    int fd;			//文件描述符
}FILE;

由上边的结构体可知,在标准C库里边定义一个FILE类型的指针,实际上是定义了一个结构体指针

实际上在标准C库中定义的FILE类型的结构体,它内部也是通过系统调用来是实现它的文件的打开、关闭、读写等操作。系统调用的最重要的是通过一个文件描述符来进行一系列的操作,在FILE类型的结构体中也能看到这个成员变量,说明fopen底层确实是通过调用open函数来实现它的功能,其他C库函数也是一样。

关于标准C库和系统调用函数的使用场景的话:由于标准C库是面向应用层的,所以它的级别会更高一点,而对于系统调用函数它由于更靠近内核,所以它的级别更低一点。所以如果在实际应用中要求响应更快,可以使用系统调用函数,因为它没有缓存,而且更靠近内核,所以它的实现速度会更快。

文件描述符

  • 在系统调用中,每一个文件都对应一个唯一的文件描述符,文件描述符是一个非负整数。当打开一个现有文件或者创建一个新的文件时,内核向进程返回一个文件描述符。后续的读写、关闭等操作都是基于这个文件描述符。

  • POSIX应用程序中,整数0、1、2被宏定义为STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,这些宏定义被定义在unistd.h头文件中

    image-20240812114934468

  • 文件描述符的范围是0-OPEN_MAX,不同的操作系统所对应的OPEN_MAX的值不相同。在终端输入getconf OPEN_MAX可以获取OPEN_MAX的值 。也就是说一个进程最多能够打开或者创建1024个文件。

    image-20240812141133454

文件描述符与文件指针的相互转换

文件描述符–>文件指针(fdopen)

FILE *fdopen(int fd, const char *mode);

//参数1:文件描述符
//参数2:权限(r 只读 w 只写)
//返回值:文件指针

文件指针–>文件描述符

int fileno(FILE *stream);

//参数:文件流指针
//返回值:文件描述符

系统调用常用函数

open函数(打开或者创建文件)

#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int oflag);
int open(const char *path, int oflag, mode_t mode);

//功能:打开或者创建一个文件
//参数1:要打开或者创建文件的路径;
//参数2:打开文件的选项,例如(O_RDONLY 只读) (O_WRONLY 只写) (O_RDWR 读写)

//第二个函数是只有文件不存在,需要创建新的文件的时候才使用
//参数1:要打开或者创建文件的路径;
//参数2:打开文件的选项,例如(O_RDONLY 只读) (O_WRONLY 只写) (O_RDWR 读写)
//参数3:创建文件要赋予的权限

//返回值是一个文件描述符

open函数的flag参数

O_RDONLY:以只读的方式打开文件

O_WRONLY:以只写的方式打开文件

O_RDWR:以读写方式打开文件

O_APPEND:以追加模式打开文件,每次写时都加到文件的尾端

O_CREAT:如果指定的文件不存在,则按mode参数指定的权限来创建文件

O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。可以用来测试一个文件是否存在。

O_DIRECTORY:如果参数pathname不是一个目录,则open出错。

O_TRUNC:如果此文件存在,而且为只读或者只写成功打开,则将其长度截断为0

o_NONBLOCK:如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为此文件的本粗打开操作和后续的I/O操作设置非阻塞方式。

creat函数(创建一个现有文件)

#include <sys/stat.h>
#include <fcntl.h>

int creat(const char *path, mode_t mode);

//参数1:要创建文件的路径
//参数2:创建文件富裕的权限
//返回值:若创建成功返回一个为只写打开的文件描述符,若出错返回-1
  • 此函数等价于open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)

  • ceate的一个不足之处时它打开的文件是为只写打开的

close函数(关闭文件)

#include <unistd.h>

int close(int fildes);

//参数:一个已经打开的文件描述符
//返回:若成功为0,若出错为-1

read函数(从打开文件中读取数据)

#include <unist.h>

ssize_t read(int fildes, void *buf, size_t nbyte);

//参数1:一个已经打开的文件描述符
//参数2:用来存放读取到的一个缓存
//参数3:要求一次读取的字节数
//返回值:读取到的字节数,若已到达文件尾为0,若出错为-1

注意:

read函数的返回值可能会少于要求读取的字节数的原因:

  • 读取普通文件时,再读到要求字节数之前已经达到了文件尾端;
  • 当从终端设备读时,通常一次最多读一行;
  • 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数;
  • 某些面向记录的设备,例如磁带,一次最多返回一个记录;
  • 进程由于信号造成中断。

read函数从文件的当前的位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。

write函数(向打开的文件中写入数据)

#include <unistd.h>

ssize_t write(int fildes, const void *buf, size_t nbyte);

//参数1:一个已经打开的文件描述符
//参数2:存放要写入文件的数据的一个缓存
//参数3:要求一次写入文件的字节数

//返回值:若成功为已写入的字节数,若出错为-1

需要注意的是:当使用write函数的时候需要特别注意的是要清楚是追加写入还是从头写入,如果想要从头写入那么就直接使用O_TRUNC标志位将文件清空,如果想要追加写入那么就使用O_APPEND标志位在文件的末尾追加写入。

lseek函数(偏移文件当前的位置)

#include <unistd.h>

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

//参数1:已经打开的文件描述符
//参数2:位移量
//参数3:定位的位置
//返回值:若成功则返回新的文件位移量(绝对偏移量(距文件开头的字节数)),若出错返回-1

参数3:whence

  • SEEK_SET:将该文件的位移量设置为距文件开始处offset个字节
  • SEEK_CUR:将该文件的位移量设置为其当前值加offsetoffset 可正可负
  • SEEK_END:将该文件的位移量设置为文件长度(文件末尾)加offsetoffset可正可负

lseek函数的注意事项

  1. lseek也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道或FIFO,则lseek返回-1,并将errno设置为EPIPE
  2. 每个打开文件都有一个与其相关联的“当前文件偏移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从当前文件偏移量处开始,并使用偏移量增加所读或所写的字节数。按系统默认,当打开一个文件时,除非指定O_APPEND追加写入或者使用lseek函数进行偏移,否则该位移量被设置为0。
代码示例:实现cp操作
#include "io.h"

#define BUFFER_SIZE 1024

void copy_function(int fdin, int fdout)
{
	ssize_t nbytes = 0;
	char buffer[BUFFER_SIZE];

	memset(buffer,'\0',BUFFER_SIZE);

	//从源文件中读取到buffer中
	while((nbytes = read(fdin, buffer, BUFFER_SIZE)) > 0)
	{
		printf("the size of read bytes is %ld\n",lseek(fdin,0,SEEK_CUR));
		//将buffer里边的数据写入到目标文件中去
		if(write(fdout, buffer, nbytes) != nbytes)
		{
			fprintf(stderr,"write error: %s\n",strerror(errno));

			exit(EXIT_FAILURE);
		}
	}

	if(nbytes < 0)
	{
		fprintf(stderr,"read error: %s\n",strerror(errno));

		exit(EXIT_FAILURE);
	}
}
//cp.c

#include "io.h"

int main(int argc, char **argv)
{
	long size;

	if(argc != 3)
	{
		fprintf(stderr,"usage: %s srcfile destfile\n",argv[0]);

		exit(EXIT_FAILURE);
	}

	int fdin, fdout;

	fdin = open(argv[1],O_RDONLY);
	if(fdin < 0)
	{
		fprintf(stderr,"open file error: %s\n",strerror(errno));

		exit(EXIT_FAILURE);
	}

	size = lseek(fdin,0,SEEK_END);

	printf("the size of file is %ld\n",size);

	fdout = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC, 0777);
	if(fdout < 0)
	{
		fprintf(stderr,"open file error: %s\n",strerror(errno));

		exit(EXIT_FAILURE);
	}

	lseek(fdin,-size,SEEK_END);
	copy_function(fdin,fdout);

	close(fdin);
	close(fdout);

	return 0;
}
//io.h

#ifndef _IO_H
#define _IO_H

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void copy_function(int fdin, int fdout);

#endif 

需要注意的是lseek函数定位的位置如果设置为SEEK_END标志位,它的偏移量是可正可负的。负方向就是从文件末尾开始向前偏移offset个字节,但如果向正方向偏移就从文件尾开始向后偏移offset个字节。如果在偏移若干个文件后再次写入数据,那么这就是一个空洞文件。

空洞文件

空洞文件是指文件中未写入数据的部分,不占用磁盘空间,直到写入数据时才会分配磁盘块。空洞文件的存在意味着文件名义上的大小可能比其占用的磁盘存储总量要大。在UNIX文件操作中,文件位移量可以大于文件的当前长度,这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞。创建空洞文件可以让多线程在不同的seek上面开始写入文件,如果不是空洞文件就只能串行写入了。例如常常在网络传输中会使用到空洞文件。当从网络上下载的时候会首先创建一个和目标文件相同大小的空洞文件,然后使用多线程在不同的地址上进行写入文件,这样能大大加快传输速度。

代码示例:空洞文件
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>

char *buffer = "1234567890";

int main(int argc, char **argv)
{
	if(argc != 2)
	{
		fprintf(stderr,"usage: %s [file]\n",argv[0]);

		exit(EXIT_FAILURE);
	}

	int fd;

	fd = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC, 0777);
	if(fd < 0)
	{
		perror("open error");
		exit(EXIT_FAILURE);
	}

	if(write(fd, buffer, strlen(buffer)) != strlen(buffer))
	{
		fprintf(stderr,"write error: %s\n",strerror(errno));

		exit(EXIT_FAILURE);
	}

	
	lseek(fd, 10, SEEK_END);
	

	if(write(fd, buffer, strlen(buffer)) != strlen(buffer))
	{
		fprintf(stderr,"write error: %s\n",strerror(errno));

		exit(EXIT_FAILURE);
	}

	close(fd);

	return 0;
}

image-20240826173407214

image-20240826173451504

缓存buffer大小设置

在进行读写操作的时候,必然会有一个缓存来存放临时的数据。实际上这个缓存的大小也是有讲究的。在数据读写的过程中,一般使用块作为一个单位进行存放,如果定义缓存大小位一个块的大小,会大大地增加传输的效率。不同系统的块大小是不一样的,可以使用以下指令查看块大小

首先使用df -h指令查看磁盘情况

image-20240813120306589

然后使用指令tune2fs查看文件系统参数(以超级用户权限)

sudo tune2fs -l /dev/sda3

image-20240813120400261

image-20240813120428456

上边的Block siez就是当前系统的块大小,将这个定义为缓存的大小有利用系统进行文件的读写操作。

  • 17
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日落星野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值