【Linux】——文件操作系统调用

一、文件I/O函数

我们所说的可用的文件I/O函数包括打开文件、读文件、写文件等。UNIX系统中大多数文件I/O需要用到5个函数:open、read、write、lseek以及close。对于内核而言,所有打开的文件都通过文件描述符引用。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或者写一个文件时,使用open或者create返回文件描述符标识该文件,将其作为参数传送给read或者write。下面,我们就来好好了解一些这五个函数。

1、open
调用open函数可以打开或创建一个文件

#include<fcntl.h>
int open(const char* filename,int flag,.../*mode_t mode */);

(1)filename:要打开或者创建文件的名字。注意!如果只是给文件名的话会在当前路径下搜索,所以确切的说应该是给定文件路径和名字。
(2)flag:表示的是文件的打开方式。可以用下列一个或多个常量进行“或”运算构成flag参数
下图的三个常量必须指定一个且只能指定一个
在这里插入图片描述而下图的常量就是可以选择的
O_APPEND:每次写时都追加到文件的尾端
O_CREAT:若此文件不存在,则创建它。但这个时候需要使用第三个参数mode,用其指定该文件的访问权限位
O_EXCL:如果同时指定了O_CREAT,而文件存在,则会出错。用这个方法可以测试一个文件是否存在,如果不存在,则创建此文件,这使得创建和测试两者成为一个原子操作。
O_TRUNC:如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0
有关同步输入输出选项的一部分
O_DSYNC:使每次write等待物理I/O操作完成,但是如果写操作并不影响读取刚写入的数据,则不等待文件属性被更新
O_RSYNC:使每一个文件描述符作为参数的read操作等待,直至任何对文件同一部分进行的未决写操作完成
O_SYNC:使每次write都等到物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O

2、close
调用close函数关闭一个打开的文件

#include<fcntl.h>
int close(int filename);

关闭一个文件时还会释放该进程加在该文件上所有记录锁。注意!当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都利用了这一功能而不显式的用close关闭打开文件。

3、lseek
可以调用lseek显式地为打开的文件设置其偏移量。每个打开的文件都有一个与其相关联的“当前文件偏移量”。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按照系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。

#include<fcntl.h>
int lseek(int fd,int size,int flag);

(1)flag:为移动标记,移动的起始位置。SEEK_SET为将该文件的偏移量设置为距文件开始处size个字节。SEEK_CUR为将文件的偏移量设置为其当前值加size,size可正可负。SEEK_END为将该文件的偏移量设置为文件长度加size,size可正可负。
(2)返回值:若lseek成功执行,则返回新的文件偏移量。注意!对于普通文件,其偏移量必须是非负数,但是某些设备也可能允许负的偏移量。所以在比较lseek的返回值时,不要测试它是否小于0,而要测试它是否等于-1。

lseek仅将当前的文件偏移量记录在内核中,他并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点其实是允许的。位于文件中但是没有写过的字节都被读为0.还要注意的一点是,对于新写的数据需要分配磁盘块,但是对于之前我们所说的空洞区域是不需要分配磁盘块的。

4、read
调用read函数从打开文件中读数据

#include<fcntl.h>
int read(int fd,void *buf,size_t size);

(1)fd:读取的文件,由open的返回值指定
(2)返回值:如果成功,返回读到的字节数。如果已经到达文件结尾,则返回0。下面的这几种情况可使实际读到的字节数少于要求读的字节数。
a.读文件时,在读到要求字节数之前已经到达了文件尾端,
b.从终端设备读时,通常一次最多读一行
c.当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数
d.当从管道或FIFO读时,如果管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数
e.当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录
f.当某一信号造成中断
(3)void*用于表示通用指针。

5、write
调用write函数向打开的文件写数据

#include<fcntl.h>
int write(int fd,void *buf,size_t size);

他的返回值与参数size的值相同,否则表示出错。其出错的常见原因是磁盘已经写满或者超过一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

6、dup和dup2函数
这两个函数都是用来复制一个现存的文件描述符

#include<unistd.h>
int dup(int fileds);
int dup2(int fileds,int fileds2);

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数制动新描述符的数值。如果filedes2已经打开,则先将其关闭。如果fileds等于fileds2,则dup2返回filedes2,而不关闭它。

7、stat,fstat和lstat函数

#include<sys/stat.h>
int stat(const char *restrict pathname,struct stat *restrict buf);
int fstat(int fileds,struct stat *buf);
int lstat(const char *restrict pathname,struct stat *restrict buf);

返回值:三个函数都是成功返回0,出错返回-1。stat返回与此命名文件有关的信息结构,fstat函数获取已在文件描述符fileds上打开文件的有关信息,lstack函数类似于stat,但是命名的文件是一个符号链接时,lstat返回该符号链接的有关信息。

二、原子操作

原子操作指的是由多步组成的操作,如果该操作原子的执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
(1)添写至一个文件
在之前所述的open操作里面其实并没有O_APPEND选项。对于单个进程是没有什么影响的,但是对于一个多进程来说同时使用这种方法将数据添加到同一文件,则会产生问题。因为各数据结构之间的关系是共享的,所以假设有两个独立的进程A和进程B都对同一个文件进行添加操作,每个进程都已经打开了该文件但是没有使用O_APPEND标志,每个进程都有他自己的文件表项,但是共享一个v结点表项,这样子两个进程的写操作会造成文件中的数据被覆盖。
逻辑操作"定位到文件尾端处和写”它使用两个分开的函数调用。解决问题的方法是是这两个操作对于其他进程而言成为一个原子操作。
(2)pread和pwrite函数
函数原型如下:

#include<unistd.h>
ssize_t pread(int flags,void *buf,size_t nbytes,off_t offset);
ssize_t pwrite(int flags,void *buf,size_t nbytes,off_t offset);

返回值:pread读到字节数,若已到文件结尾则返回0,若出错则返回-1;pwrite若成功返回已写的字节数,若出错则返回-1.
调用pread相当于顺序调用lseek和read,调用pwrite相当于顺序调用lseek和write

三、实例

实践一:
有了以上执行I/O操作的基本函数,下面我们就来实践一下,实现将用户在界面上输入的数据存储到a.txt中,再将a.txt中所有内容整体显示到终端上

int main()
{
	int fd = open("a.txt", O_RDWR | O_CREAT, 0664);//权限设置值
	assert(-1 != fd);
	
	while(1)
	{
		printf("input: ");
		char buff[128] = {0};
		fgets(buff,128,stdin);//从用户获取数据,stdin标准输入,会把最后的回车符也放在buff中
		
		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}
		
		int n =write(fd,buff,strlen(buff));
		if(n<=0)
		{
			perror("write error:");//和printf很像,但是他主要是打的出错信息
			exit(0);
		}
	}
	
	printf(****************************a.txt:*************************\n);
	lseek(fd,0,SEEK_SET);//将文件读写游标移动到开始位置
	
	while(1)
	{
		char buff[128] = {0};//从文件里面读取数据往buff中写
		int n = read(fd,buff,127);
		if( n == 0 )
		{
			printf("END\n");
			break;
		}
		else if(n<0)
		{
			perror("read error: ");
			exit(0);
		}
		else
		{
			printf("%s",buff);
		}
	}
	close(fd);
}

实践二:
通过代码测试父子进程共享fork之前打开的文件描述符

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>

int main()
{
	int fd = open("a.txt", O_RDWR | O_CREAT, 0664);//权限设置值
	assert(-1 != fd);

	pid_t n = fork();
	assert(-1 != n);

	if(0 == n)
	{
		while(1)
		{
			char c = 0;
			int len = read(fd, &c, 1);
			if(n <= 0)
			{
				break;
			}
			printf("child:: %c\n", c);
			sleep(1);
		}
	}
	else
	{
		while(1)
		{
			char c = 0;
			int len = read(fd, &c, 1);
			if(n <= 0)
			{
				break;
			}
			printf("father:: %c\n", c);
			sleep(1);
		}
	}

	close(fd);

	exit(0);
}

两次不同的执行结果如下:
在这里插入图片描述
由以上的执行结果我们可以得到对于fork之前打开的文件描述符,父子进程都可以访问,并且共享文件读写偏移量,fork之后在父进程当中打开的文件描述符不共享。其内部实现过程如下:
在这里插入图片描述

四、库函数和系统调用函数的区别

(1)概念
首先,我们要明确一下什么是库函数什么是系统调用函数。在之前我们学习的fopen,fread,fwrite,fclose还有fseek都是我们所指的库函数,而在这篇文章开头我们就列出来的read,write,close等都是系统调用函数。例如,经常我们会碰到这样一个问题,fread和read哪一个的效率更高?其实不是绝对的read效率高,在读取的文件很少的情况下,因为fread会存在从用户态到内核态的调用消耗,但是对于要读大量的数据,fread的操作是一次性把所有的放入用户访问区用户用多少取多少但是read的操作是有多少数据读多少数据。
由此就可以引出我们库函数和系统调用函数的概念了。系统调用函数是系统内核跑出来给用户空间调用的接口,系统调用函数由用户态调用,在内核态执行。与之对应的就是库函数了,库函数在函数库文件中实现,执行时只需要在用户态执行就可以了。

(2)区别
其实在概念里面我们就可以很明确的知道他们的区别了,库函数在函数库文件中,系统调用函数在系统内核中实现。接下来,我们仔细的以open为列讲解系统调用函数的实现原理。
在我们一个系统中,他们之间的关系如下图所示:
在这里插入图片描述
1、首先找到函数对应的系统调用号,把其保存到exa寄存器
2、系统调用函数触发0x80中断,然后陷入内核,内核开始执行中断处理程序。0x80中断的重要指令如下

call [_sys_call_table+eax*4]

这个指令就是主要让之前我们存在eax寄存器当中的系统调用号在内核系统调用表中查找内核函数方法并执行。
3、函数调用完毕之后会有一个return fd一个整形值,把该整形值放入eax寄存器当中然后再切换到用户态中,再一个mov指令把eax寄存器的值移动到fd所指向的地址上,这样相当于保存了函数的返回值。
具体过程图示如下:
在这里插入图片描述

发布了98 篇原创文章 · 获赞 9 · 访问量 3641

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105460239