1. 文件描述符与文件指针的关系
文件描述符:每个进程的PCB里都有任务描述符表,任务描述符表其实是个数组,里面存放一个指针,指向已打开文件的file结构体,该结构体包含文件当前的位移量、打开模式、inode指针等信息。系统调用的操作对象为文件描述符。
当开始运行程序时,也就是系统开始运行时,它一般会有三个已经打开的文件描述符。他们是:
0:标准输入
1:标准输出
2:标准错误
文件指针:指向一个FILE的结构体,这个结构体里包括一个文件描述符(在Windows下也被称为文件句柄)和一个I/O缓冲区。库函数的操作对象是文件指针。
同样,s t d i n , s t d o u t和s t d e r r这三个文件指针定义在头文件< s t d i o . h >中
2.系统调用与库函数
Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions)。可以参考《Linux程序设计》(英文原版为《Beginning Linux Programming》,作者是Neil Matthew和Richard Stones)第三章: Working with files。系统调用实际上就是指最底层的一个调用,在linux程序设计里面就是底层调用的意思。面向的是硬件。而库函数调用则面向的是应用开发的,相当于应用程序的api,采用这样的方式有很多种原因,第一:双缓冲技术的实现。第二,可移植性。第三,底层调用本身的一些性能方面的缺陷。第四:让api也可以有了级别和专门的工作面向。
系统调用
系统调用提供的函数如open, close, read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为 size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如 fd=open(\"/dev/video\", O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。
系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。
系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。
系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。
这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。
用户缓冲是为了降低系统调用的次数,减少用户态到内核态的切换,内核缓冲是为了降低操作硬件的次数。
库函数调用
标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose, fflush, fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_t fwrite(const void *buffer, size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen(\"~/proj/filename\", \"w\")。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。
库函数调用通常用于应用程序中对一般文件的访问。
库函数调用是系统无关的,因此可移植性好。
由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作。
※函数库调用 VS 系统调用
函数库调用
系统调用
在所有的ANSI C编译器版本中,C库函数是相同的
各个操作系统的系统调用是不同的
它调用函数库中的一段程序(或函数)
它调用系统内核的服务
与用户程序相联系
是操作系统的一个入口点
在用户地址空间执行
在内核地址空间执行
它的运行时间属于“用户时间”
它的运行时间属于“系统”时间
属于过程调用,调用开销较小
需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数
在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc
典型的系统调用:chdir fork write brk;
3.系统调用函数
因为每个read, write都因调用系统调用而进入内核,所以称这些函数为不带缓存的 I / O函数。这里的缓存指的是用户级缓冲。
所有的系统调用:https://blog.csdn.net/orange_os/article/details/7485069
open系统调用
系统调用open的作用是打开一个文件,并返回这个文件的描述符。
简单地说,open建立了一条到文件或设备的访问路径。如果操作成功,它将返回一个文件描述符,read和write等系统调用使用该文件描述符对文件或设备进行操作。这个文件描述符是唯一的,他不会和任何其他运行中的进程共享。如果两个程序同时打开一个文件,会得到两个不同的文件描述符。
如果同时对指向同一文件的两个文件描述符进行操作,他们各自操作,互不影响,彼此相互覆盖(后写入的覆盖先写入的)为了防止文件按读写冲突,可以使用文件锁的功能。这不是
本次重点,以后介绍。
Linux中open的函数原型有两个:
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode );
参数说明。
path:准备打开的文件或设备名字。
oflags:指出要打开文件的访问模式。open调用必须指定如下所示的文件访问模式之一:
open调用哈可以在oflags参数中包括下列可选模式的组合(用”按位或“操作):
- O_APPEDN: 把写入数据追加在文件的末尾。
- O_TRUNC: 把文件长度设为零,丢弃以后的内容。
- O_CREAT: 如果需要,就按参数mode中给出的访问模式创建文件。
- O_EXCL: 与O_CREAT一起调用,确保调用者创建出文件。使用这个模式可防止两个程序同时创建一个文件,如果文件已经存在,open调用将失败。
关于其他可能出现的oflags值,请看考open的调用手册。
mode:
当使用哦、O_CREAT标志的open来创建文件时,我们必须使用三个参数格式的open调用。第三个参数mode 是几个标志按位OR后得到的。他们是:
- S_IRUSR: 读权限,文件属主。
- S_IWUSR:写权限,文件属主。
- S_ IXUSR:执行权限,文件属主。
- S_IRGRP:读权限,文件所属组。
- S_IWGRP:写权限,文件所属组。
- S_IXGRP:写权限,文件所属组。
- S_IROTH:读权限,其它用户。
- S_IWOTH:写权限,其它用户。
- S_IXOTH:写权限,其它用户。
。。。
请看下面例子:
open("myfile", O_CREAT, S_IRUSR|S_IXOTH ;//路径名,文件名最好带双引号
他的作用是创建一个名为myfile 的文件,文件属主拥有读权限,其他用户拥有执行权限,且只有这些权限。
运行结果:
程序创建了一个名为myfile的文件,文件属主有读权限,其他用户有执行权限,且只有这些权限。
除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以自己用数字来表示,Linux总共用5个数字来表示文件的各种权限:第一位表示设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;最后一位表示其他人的权限。每个数字可以取1(执行权限)、2(写权限)、4(读权限)、0(无)或者是这些值的和。例如,要创建一个用户可读、可写、可执行,但是组没有权限,其他人可以读、可以执行的文件,并设置用户ID位。那么,我们应该使用的模式是1(设置用户ID)、0(不设置组ID)、7(1+2+4,读、写、执行)、0(没有权限)、5(1+4,读、执行)即10705:
open("test", O_CREAT, 10705);
上述语句等价于:
open("test", O_CREAT, S_IRWXU | S_IROTH | S_IXOTH | S_ISUID );
close系统调用
close系统调用用于“关闭”一个文件,close调用终止一个文件描述符fildes以其文件之间的关联。文件描述符被释放,并能够重新使用。
close成功返回1,出错返回-1.
#Include<unistd.h>
int close(int fildes);
read系统调用
系统调用read是从文件中读出数据。要读取的文件用文件描述符标识,数据读入一个事先定义好的缓冲区。他返回实际读入的字节数。
Linux中read的函数原型:
size_t read(int fildes, void *buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识要读取的文件。如果为0,则从标准输入读数据。类似于scanf()的功能。
*buf:缓冲区用来存储读入的数据。
返回值:size_t返回成功读取的字符数,它可能会小于请求的字节数。
运行结果:
write系统调用
write,就是把缓冲区的数据写入文件中。注意,这里的文件时广泛意义的文件,比如写入磁盘、写入打印机等等。
Linux 中write()的函数原型:
size_t write(int fildes, const void *buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识了要写入的目标文件。例如:fildes的值为1,就像标准输出写数据,也就是在显示屏上显示数据;如果为 2 ,则想标注错误写数据。
*buf:待写入的文件,是一个字符串指针或字符串。
nbytes:要写入的字符数。
函数返回值:size_t 返回成功写入文件的字符数。需要指出的是,write可能会报告说他写入的字节比你所要求的少。这并不一定是个错误。在程序中,你需要检查
error已发现错误,然后再次调用write写入剩余的数据。
请看下面的例子:
运行结果:
这个程序只在标准输出上显示一条消息。
lseek系统调用
对于随机文件,我们可以随机的指定位置读写,使用如下函数进行定位:
int lseek(int fd, offset_t offset, int whence);
lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的位置。参数whence可使用下述值:
SEEK_SET:相对文件开头
SEEK_CUR:相对文件读写指针的当前位置
SEEK_END:相对文件末尾
offset可取负值,例如下述调用可将文件指针相对当前位置向前移动5个字节:
lseek(fd, -5, SEEK_CUR);
由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度:
lseek(fd, 0, SEEK_END);
ioctl系统调用
ioctl提供了一个用于控制设备及其描述符行为和配置底层服务的接口。终端、文件描述符、甚至磁带机都可以有为他们定义的ioctl,具体
细节可以参考特定设备的使用手册。
下面是ioctl 的函数原型
#include<unistd.h>
int ioctl(int fildes, int cmd,,,,,,);
ioctl对描述符fildes指定的对象执行cmd 参数中所给出的操作。
其他和文件管理有关的系统调用
还有许多其他的系统调用能对文件进行操作。
几个常用的如:lseek()对文件描述符fildes指定文件的读写指针进行设置,也就是说,它可以设置文件的下一个读写位置。
fstat,stat,lstat 是和文件描述符相关的函数操作,这里就不做介绍。
dup,dup2系统调用。dup提供了复制文件描述符的方法,使我们能够通过两个或者更多个不同的文件描述符来访问同一个文件。这可以用于
在文件的不同位置对数据进行读写。
stat系统调用:
显示文件的相关信息(类似于 ls -l的感觉)
头文件及函数原型:
函数参数:path:文件的路径,buf是指待写入的文件信息,fd:表示文件描述符;
stat,fstat,lstat三者的区别在于:fstat是系统调用函数,不可以移植,第一个参数为文件描述符,需要用open的返回值获得。其他两个的第一个参数均为文件的路径,三个函数的第二个参数均是一个结构体,用来表示文件的各种信息;另外lstat(带l的具不具备穿透性)不具有穿透性,即如果打开的文件是一个软连接(快捷方式),它不具备追踪到最终的文件功能。
返回值:成功(0),失败(-1);
结构体stat如下:
其中重要参数:st_mode代表着文件的权限和文件的类型,其结构如下:
实例及发现
- fd=open(…);假如此时的fd为3,Close(fd)之后,fd不会变化,仍然等于3,但是fd1=open(…)之后,3会被重新分配给fd1;
- 系统调用删除一个文件:unlink(“filename”);(硬链接数如果为1的话,调用一次Unlink就可以删除);
- Read\write的buffer是指存储读出内容或待写入内容,这两个函数可以操作一般文件和I/O;函数的返回值为size_t类型,在使用printf输出时,可用%zu来表示;
- 使用write时,有时会输出方框,@,是因为write的size大于buffer里存的size;
- 代码:
#include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <stdio.h> int main() { char buffer[20]; size_t readcount = 0; size_t writecount = 0; int openfd = 0; int openfd1 = 0; int createfd1 = 0; int createfd2 = 0; openfd = open("a.txt",O_RDONLY); createfd1 = open("haha",O_CREAT|O_EXCL,11777); createfd2 = open("haha",O_CREAT|O_EXCL,11777); printf("openfd,createfd1,createfd2:%d,%d,%d \n",openfd,createfd1,createfd2); //lseek(0,10,SEEK_CUR); readcount = read(0,buffer,20); writecount = write(1,buffer,20); printf("readcount,writecount:%zu,%zu \n",readcount,writecount); close(openfd); close(createfd1); unlink("haha"); openfd1 = open("a.txt",O_RDONLY); printf("openfd1:%d \n",openfd1); }
4. 库函数
标准I/O库及其头文件<stdio.h>为底层I/O系统调用提供了一个通用的接口。这个库现在已经成为ANSI标准C的一部分,而前面所讲的系统调用却不是。
标准I/O库提供了许多复杂功能的函数,用于格式化输出和扫描输入,它还负责满足设备的缓冲需求。
在许多方面,使用标准I/O库和使用底层文件描述符类似。需要先打开一个文件,已建立一个文件访问路径(也就是系统调用中的文件描述符)
在标准I/O库中,与文件描述符对应的叫 流(stream),它被实现为指向结构FILE的指针。
在启动程序时,有三个文件流是自动打开的。他们是:
- stdin: 标准输入
- stdout: 标准输出
- stderr: 标准错误输出
下面会介绍一些常用的I/O库函数:
fopen函数
fopen函数类似于系统调用中的open函数。和open一样,它返回文件的标识符,只是这里叫做流(stream),在库函数里实现为一个指向文件的指针。
如果需要对设备的行为进行明确的控制,最好使用底层系统调用,因为这可以避免使用库函数带来的一些非预期的副作用,如输入/输出缓冲。
函数原型:
#include<stdio.h>
FILE *fopen(const char *filename, const char *mode);//如果参数是一个指针,则实际参数可放一个字符串,传入的字符串地址即是该指针。
参数说明:
*filename:打开文件的文件名
*mode:打开的方式
字符 含义
────────────────────────────
"r" 打开文字文件只读
"w" 创建文字文件只写
"a" 增补, 如果文件不存在则创建一个
"r+" 打开一个文字文件读/写
"w+" 创建一个文字文件读/写
"a+" 打开或创建一个文件增补
"b" 二进制文件(可以和上面每一项合用)
"t" 文这文件(默认项)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
文件使用方式 意 义
“rt” 只读打开一个文本文件,只允许读数据
“wt” 只写打开或建立一个文本文件,只允许写数据
“at” 追加打开一个文本文件,并在文件末尾写数据
“rb” 只读打开一个二进制文件,只允许读数据
“wb” 只写打开或建立一个二进制文件,只允许写数据
“ab” 追加打开一个二进制文件,并在文件末尾写数据
“rt+” 读写打开一个文本文件,允许读和写
“wt+” 读写打开或建立一个文本文件,允许读写
“at+” 读写打开一个文本文件,允许读,或在文件末追加数 据
“rb+” 读写打开一个二进制文件,允许读和写
“wb+” 读写打开或建立一个二进制文件,允许读和写
“ab+” 读写打开一个二进制文件,允许读,或在文件末追加数据
对于文件使用方式有以下几点说明:
1. 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:
r(read): 读
w(write): 写
a(append): 追加
t(text): 文本文件,可省略不写
b(banary): 二进制文件
+: 读和写
2. 凡用“r”打开一个文件时,该文件必须已经存在, 且只能从该文件读出。
3. 用“w”打开的文件只能向该文件写入。 若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。
4. 若要向一个已存在的文件追加新的信息,只能用“a ”方式打开文件。
5. 在打开一个文件时,如果出错,fopen将返回一个空指针值NULL。在程序中可以用这一信息来判别是否完成打开文件的工作,并作相应的处理。fopen在成功时返回一个非空的FILE *指针。失败返回NULL
fclose函数
fclose函数关闭指定的文件流stream,这个操作会使所有未写出的数据都写出。因为stdio库函数会对数据进行缓冲,所有调用fclose函数是很重要的。
如果程序需要确保数据已经全部写出,就应该调用fclose函数。虽然程序正常结束时,也会自动的调用fclose函数,但这样就不能检测出调用fclose所产生的错误了。
函数原型如下:
#include<stdio,h>
int fclose(FILE *stream);
fread/fwrite函数
fread函数从文件流中读取数据,对应于系统调用中的read;fwrite函数从文件流中写数据,对应于系统调用中的write
函数原型:
#include<stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
参数说明:
*ptr 要读取数据的缓冲区,也就是要存放读取数据的地方。
size:指定每个数据记录的长度。
nitems: 计数,给出要传输的记录个数。//什么意思?
返回值:成功读取到数据缓冲区的记录个数,当到达文件尾时,他的返回值可能会小于nitems,甚至可以是0
size_t fwrite(const coid *ptr, size_t size , size_t nitimes, FILE *stream);
他从指定的数据缓冲区ptr中把数据写入文件流,返回成功写入的记录个数。
fseek函数
int fseek(FILE *stream, long offset, int whence);
fflush函数
fflush函数的作用是把文件流中所有未写出的数据全部写出。 处于效率考虑,在使用库函数的时候会使用数据缓冲区,当缓冲区满的时候才进行写操作。使用fflush函数
可以将缓冲区的数据全部写出,而不关心缓冲区是否满。fclose的执行隐含调用了fflush函数,所以不必再fclose执行之前调用fflush。
函数原型:
#include<stdio.h>
int fflush(FILE *stream);
其它读/写库函数
C库函数支持以字符、字符串等为单位,支持按照某中格式进行文件的读写,这一组函数为:
int getc(FILE *stream);
int putc(int ch, FILE *fp);;
int getchar(void);
int putchar(int char);
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
char *fgets(char *s, int n, FILE *stream);
int fputs(const char *s, FILE *stream);
int printf(const char *format, ...)
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *string, char *format [,argument,...]);
int snprintf(char *str, size_t size, const char *format, ...);
int vprintf(char *format, va_list param);
int vfprintf(FILE *stream, char *format, va_list param);
int vsprintf(char *string, char *format, va_list param);
int _vsnprintf(char* str, size_t size, const char* format, va_list ap);
..............
int fscanf (FILE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
size_t fwrite (const void *ptr, size_t size, size_t n, FILE *stream);
C库函数还提供了读写过程中的定位能力,这些函数包括
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
int fseek(FILE *stream, long offset, int whence);
等。
缓冲
缓冲可以分为:用户程序缓冲、C标准I/o缓冲、内核缓冲
标准I / O提供了三种类型的缓存:
(1) 全缓存。在这种情况下,当填满标准I / O缓存后才进行实际I / O操作。对于驻在磁盘上的文件通常是由标准I / O库实施全缓存的。在一个流上执行第一次I / O操作时,相关标准I / O函数通常调用m a l l o c(见7 . 8节)获得需使用的缓存。
术语刷新(f l u s h)说明标准I / O缓存的写操作。缓存可由标准I / O例程自动地刷新(例如当填满一个缓存时),或者可以调用函数 ff l u s h刷新一个流。值得引起注意的是在 U N I X环境中,刷新有两种意思。在标准 I / O库方面,刷新意味着将缓存中的内容写到磁盘上(该缓存可以只是局部填写的)。在终端驱动程序方面(例如在第11章中所述的t c f l u s h函数),刷新表示丢弃已存在缓存中的数据。
(2) 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准 I / O库执行I / O操作。这允许我们一次输出一个字符(用标准 I/O fputc函数),但只有在写了一行之后才进行实际 I / O操作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓存。
对于行缓存有两个限制。第一个是:因为标准 I / O库用来收集每一行的缓存的长度是固定
的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行 I / O操作。第二个是:任何时候只要通过标准输入输出库要求从 ( a )一个不带缓存的流,或者 ( b )一个行缓存的流(它预先要求从内核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。在 ( b )中带了一个在括号中的说明的理由是,所需的数据可能已在该缓存中,它并不要求内核在需要该数据时才进行该操作。很明显,从不带缓存的一个流中进行输入( ( a )项)要求当时从内核得到数据。
(3) 不带缓存。标准I / O库不对字符进行缓存。如果用标准 I / O函数写若干字符到不带缓存的流中,则相当于用 w r i t e系统调用函数将这些字符写至相关联的打开文件上。标准出错流s t d e r r通常是不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。
对普通文件,默认的都是全缓冲。对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓存类型:
#include <stdio.h>
void setbuf(FILE *f p, char * b u f ) ;
int setvbuf(FILE *f p, char *b u f, int m o d e, size_t s i z e) ;
返回:若成功则为0,若出错则为非0
实例
#include <stdio.h> int main() { FILE* fp = NULL; char buf[50]; size_t readcount = 0; size_t writecount = 0; fp = fopen("a.txt","a+"); printf("fp:%p \n",fp); readcount = fread(buf,4,3,fp); printf("readcount:%zu \n",readcount); writecount = fwrite(buf,4,3,stdout); printf("writecount:%zu \n",writecount); fclose(fp); }