Linux文件操作—底层文件访问和标准I/O库以及两者的区别

底层文件访问

每个运行中的程序被称为进程,它有一些与之关联的文件描述符,我们可以根据文件描述符访问打开的文件或设备,当一个程序运行时,它一般会有3个已经打开的文件描述符。

  • 0:标准输入
  • 1:标准输出
  • 2:标准错误

我们可以通过系统调用open把其他的文件描述符与文件和设备相关联,自动打开的文件描述符就已经可以通过write系统调用来创建一些简单的程序了。

1.write系调用

系统调用write的作用是把缓冲区buf的前nbytes字节写入与文件描述符fildes相关联的文件中。它返回实际写入的字节数。如果文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值会小于nbytes。如果返回值为0,就表示未写入任何数据,如果返回值为-1表示在write调用中出现了错误,错误代码保存在全局变量errno中。

下面是write系统调用的原型:

#include <unistd.h>

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

注意:wirte可能会报告写入的字节比你要求的少,这并不一定是个错误。在程序中,你需要检查errno以发现错误,然后再次调用write写入剩余的数据。

2.read系统调用

系统调用read作用是从与文件描述符fildes相关联的文件里读入nbytes个字节的数据,并把它们放到数据区buf中。它返回实际读入的字节数,这可能会小于请求的字节数。如果read调用返回0,则表示未读入任何数据,已经到达了文件尾。如果返回的是-1,则read调用出现错误。

read系统调用原型:

#include <unistd.h>

size_t read(int fildes, void *buf, size_t nbytes);

3.open系统调用

为了创建一个新的文件描述符,便需要使用open系统调用。简单的说,open建立了一条到文件或设备的访问路径。如果调用成功,它返回一个可以被read、write和其他系统调用使用的文件描述符。**这个文件描述符是唯一的,它不会与任何其他运行中的程序共享。**如果两个程序同时打开同一个文件,它们会分别得到两个不同的文件描述符。
准备打开的文件或设备的名字作为参数path传递给函数,oflags参数用于指定打开文件所采取的动作。

open系统调用原型:

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

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

在这里插入图片描述
下面我们给出一个例子:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>   

int main()
{
	int fd = open("1.txt",O_WRONLY|O_CREAT);
    write(fd,"hello",6);
    close(fd);
}

从上面可以看出我们先进行了open系统调用然后进行了write系统调用并往1.txt文件中写入hello,但是我们在控制台输入cat 1.txt命令时出现了下面的情况:
在这里插入图片描述
从上图我们可以看到权限不够的错误提示,输入ls -ls 1.txt命令后我们可以发现并没有给属主赋予读权限所以导致我们cat命令失败。其原因在于我们在进行open系统调用时并没有赋予访问权限的初始值。

在我们使用带有O_CREAT标志的open调用来创建文件时,必须使用有三个参数格式的open调用,也就是说需要使用函数原型:
int open(const char *path, int oflags, mode_t mode);
我们将代码修改后如下:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>   

int main()
{
	int fd = open("1.txt",O_WRONLY|O_CREAT,0600);
    write(fd,"hello",6);
    close(fd);
}

运行后结果如下:
在这里插入图片描述
其中0600表示分配给文件的权限一共四位数,第一位数表示gid/uid一般不用,剩下三位分别表示user(属主),group(属组),other(其他用户)的权限。每个数可以转换为三位二进制数,分别表示rwx(读,写,执行)权限,为1表示有权限,0无权限如6是上面第二个数,可以表示为二进制数110,表示user有读,写权限,无执行权限。正对应上图的 -rw--------

任何一个运行中的程序能够同时打开的文件数是有限制的。这个限制通常是由limits.h头文件中常量OPEN_MAX定义的,它的值随系统的不同而不同,但POSIX要求它至少为16。在linux系统中这个限制可以在系统运行时调整,所以OPEN_MAX并不是一个常量。它通常一开始设置为256

4.close系统调用

close系统调用意在终止文件描述符与其对应文件的关联使得文件描述符被释放并能够重新使用。close系统调用成功返回0,出错返回-1.
函数原型:

#include <unistd.h>

int close(int fildes);

注意:检查close调用的返回结果非常重要。有的文件系统,特别是网络文件系统,可能不会在关闭文件之前报告文件写操作中出现的错误,这是因为在执行写操作时,数据可能未被确认写入。

其他系统调用

还有许多其他的系统调用能够操作这些底层文件描述符。通过它们,程序可以控制文件的使用方式和返回文件的状态信息。

1.lseek系统调用

lseek系统调用对文件描述符fildes的读写指针进行设置。也就是说,你可以用它来设置文件的下一个读写位置。读写指针既可以被设置为文件中的某个绝对文字,也可以把它设置为相对于当前位置或文件尾的某个相对位置。
原型:

#include <unistd.h>
#include <sys/types.h>

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

offset参数用来指定位置,而whence参数定义该偏移值的用法。whence可以取下列值之一。

  • SEEK_SET : offset是一个绝对位置。
  • SEEK_CUR : offset是相对于当前位置的一个相对位置。
  • SEEK_END : offset是相对于文件尾的一个相对位置。
    lseek返回从文件头到文件指针被设置出的字节偏移值的,失败时返回-1.参数offset的类型off_t是一个与具体实现有关的整数类型,它定义在头文件sys/types.h中。

2.fstat、stat和lstat系统调用

fstat系统调用返回与打开的文件描述符相关的文件的状态信息,该信息将会写到一个buff结构中,buff的地址以参数形式传递给fstat。
原型:

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

int fstat(int fildes, struct stat *buf);
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);

相关函数stat和lstat返回的是通过文件名查到的状态信息。它们产生相同的结果,但当文件是一个符号链接时,lstat返回的是该符号链接本身的信息,而stat返回的是该链接指向的文件的信息。

3.dup和dup2系统调用

dup系统调用提供了一种复制文件描述符的方法,使我们能够通过两个或者更多个不同的描述符来访问同一个文件。这可以用于在文件的不同位置对数据进行读写。dup系统调用复制文件描述符fildes,返回一个新的描述符。dup2系统调用则是通过明确指定目标描述符来吧一个文件描述符复制为另一个。
原型如下:

#include <unist.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);

标准I/O库

在很多方面,使用I/O库的方式和底层文件描述符是一样的。需要先打开一个文件建立一个访问路径。这个操作的返回值将作为其他I/O库函数的参数。在标准I/O库中,与底层文件描述符对应的是流,它被实现为指向结构FILE的指针。

在启动程序时,有三个文件流是自动打开的。

  • stdin:标准输入
  • stdout:标准输出
  • stderr:标准错误输出

1.fopen函数

fopen库函数类似于底层的open系统调用。它主要用于文件和终端的输入输出。如果需要对设备进行明确的控制,那最好使用底层系统调用,因为这可以避免用库函数带来的一些潜在的问题,如输入/输出缓冲。

函数原型:

#include <stdio.h>

FILE *fopen(const char *filename, const char *mode);

在这里插入图片描述
fopen成功时返回一个非空的FILE *指针,失败时返回NULL值,NULL值在头文件stdio.h中定义。
可用的文件流数量和文件描述符一样,都是有限制的,实际限制是由头文件stdio.h中FOPEN_MAX定义的,至少为8,在linux系统中,通常是16。

2.fread函数

fread库函数用于从一个文件流里读数据。数据从文件流stream督导ptr指向的数据缓冲区里。fread和fwrite都是对数据记录进行操作,size参数指定每个数据记录的长度,计数器nitems给出要传输的记录个数,它的返回值是成功读到缓冲区里的记录个数(而不是字节数)。当到达文件尾时,它的返回值可能小于nitems,甚至可以是零。
函数原型如下:

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

3.fwrite函数

fwrite库函数与fread函数有相似的接口。它从指定的数据缓冲区里取出数据记录,并把它们写道输出流中。它的返回值是成功写入的记录个数。

函数原型如下

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);

4.fclose函数

fclose函数关闭指定的文件流stream,使所有尚未写出的数据都写出。因为stdio库会对数据进行缓冲,所以fclose是很重要的。如果程序需要确保数据已经全部写出,就应该调用fclose函数。虽然当程序正常结束时,会自动对所有还打开的文件流调用fclose函数,但这样做你就没有机会检查由fclose报告的错误了。

函数原型如下:

#include <stdio.h>

int fclose(FILE *stream);

5.fseek函数

fseek函数是与lseek系统调用对应的文件流函数。它在文件流里为下一次读写操作指定位置,offset和whence参数的含义和取值与前面lseek系统调用相同,fseek返回的是一个整数,0表示成功,-1表示失败并设置errno指出错误。

函数原型如下:

#include <stdio.h>

int fseek(FILE *stream, long int offset, int whence);

库函数示例:

#include<stdio.h>

int main()
{
	FILE *fp;
	int a = 45;
	int b;
	fp=fopen("1.txt","wb+");

	fwrite(&a,sizeof(int),1,fp);///将a值写到文件中.这句起作用,移动了文件指针。
	fseek(fp,0,0);//将文件指针移回首部
	fread(&b,sizeof(int),1,fp);

	printf("b=%d\n",b);

	fclose(fp);

	return 0;
}

系统调用和库函数的区别

系统调用,我们可以理解是操作系统为用户提供的一系列操作的接口(API),这些接口提供了对系统硬件设备功能的操作。这么说可能会比较抽象,举个例子,我们最熟悉的 hello world 程序会在屏幕上打印出信息。程序中调用了 printf() 函数,而库函数 printf 本质上是调用了系统调用 write() 函数,实现了终端信息的打印功能。

库函数可以理解为是对系统调用的一层封装。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员。

库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。可以参考下图来理解库函数与系统调用的关系。
在这里插入图片描述
两者区别如下:

库函数调用是语言或应用程序的一部分,而系统调用是操作系统的一部分。

  • 所有 C 库函数是相同的,而各个操作系统的系统调用是不同的。
  • 库函数调用是调用库函数中的一个程序,而系统调用是调用系统内核的服务。
  • 库函数调用是与用户程序相联系,而系统调用是操作系统的一个进入点
  • 库函数调用是在用户地址空间执行,而系统调用是在内核地址空间执行
  • 库函数调用的运行时间属于「用户」时间,而系统调用的运行时间属于「系统」时间
  • 库函数调用属于过程调用,开销较小,而系统调用需要切换到内核上下文环境然后切换回来,开销较大
  • 在C函数库libc中大约 300 个程序,在 UNIX 中大约有 90 个系统调用

据书中记载,库函数调用大概花费时间为半微妙,而系统调用所需要的时间大约是库函数调用的 70 倍(35微秒)。这是因为系统调用会产生用户态—>内核态—>用户态的开销。但并不是说所有情况下我们都率先使用库函数,只是在有库函数有缓冲区的情况下我们优先使用,因为缓冲区可以有效的减少系统调用的次数

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值