文件系统编程—系统调用与标准IO

时间:2022-12-15 10:04:29

首先需要明白的是,所有的操作系统都提供多种服务的入口点,程序由此向内核请求服务。这些可直接进入内核的入口点被称为系统调用

在Linux中,为了更好地保护内核空间,程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。

相关的一些函数:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
//Returns file descriptor on success, or –1 on error

各参数及返回值的含义如下:
pathname:要打开或创建的文件名称。
flags:标志位,指定打开文件的操作方式。
mode:指定新文件的访问权限。(仅当创建新文件时才使用该参数)
返回值:若成功返回文件描述符,否则返回-1并设置变量errno的值。

这里看到这么多的头文件以及要记这么多的参数是不是很困难,这里可以有个很好的方法,在Linux系统下的终端中打出 man open,之后就会弹出这个open()函数的所有使用方法,但这个里面全是英文的说明,有需要的小伙伴可以去网上下载中文版的Linux中文man离线手册,很不错。

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//Returns number of bytes read, 0 on EOF, or1 on error

各参数及返回值的含义如下:
fd:要读取的文件的描述符。
buf:读取到的数据要放入的缓冲区。
count:要读取的字节数。
返回值:若成功返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1并设置变量errno的值。
注意:
1. 这里的size_t是无符号整型,ssize_t是有符号整型。
2. buf指向的内存空间必须事先分配好。

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
//Returns number of bytes written, or –1 on error

各参数及返回值的含义如下:
fd:要写入的文件的描述符。
buf:要写入的数据所存放的缓冲区。
count:要写入的字节数。
返回值:若成功返回已写的字节数,出错则返回-1并设置变量errno的值。

#include <unistd.h>
int close(int fd);
//Returns 0 on success, or –1 on error

各参数及返回值的含义如下:
fd:要关闭的文件的描述符。
返回值:若成功返回0,出错则返回-1。
注:当一个进程终止时,内核会自动关闭它所有打开的文件。

以及lseek()函数等。

从以上对文件的各种操作中,是不是可以推测出文件描述符和打开的文件是一一对应的关系?
为了搞清楚上述问题,首先需要了解内核如何表示打开的文件
内核使用三种数据结构表示一个打开的文件!
文件描述符表(每个进程都有)
(1) 文件描述符标志(file descriptor flags),如:close_on_exec。
(2) 指向一个文件表项(file table entry)的指针。
打开文件表(open file table)
(1) 文件状态标志(file status flags),如读、写、非阻塞等。
(2) 当前文件偏移量(file offset)。
(3) 文件访问模式(read-only, write-only, or read-write)。
(4) 指向i-node表项的指针。

下面来说说标准I/O,C标准库提供了操作文件的标准I/O函数库,与系统调用相比,主要差别是实现了一个跨平台的用户态缓冲的解决方案。 系统调用要请求内核的服务,会引发CPU模式的切换,期间会有大量的堆栈数据保存操作,开销比较大。如果频繁地进行系统调用,会降低应用程序的运行效率。有了缓冲机制以后,多个读写操作可以合并为一次系统调用,减少了系统调用的次数,将大大提高程序的运行效率。

所谓的标准I/O函数实际上是对底层系统调用的封装,最终读写设备或文件的操作仍需调用系统I/O函数来完成。
标准I/O函数并不直接操作文件描述符,而是使用文件指针。文件指针和文件描述符是一一对应的关系,这种对应关系由标准I/O库自己内部维护。文件指针指向的数据类型为FILE型,但应用程序无须关心它的具体内容。
在标准I/O中,一个打开的文件称为流(stream),流可以用于读(输入流)、写(输出流)或读写(输入输出流)。每个进程在启动后就会打开三个流,分别对应:stdin(标准输入流)、stdout(标准输出流)以及stderr(标准错误输出流)。

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

fopen打开由path指定的文件,并把它与一个文件流关联起来。mode参数指定文件的打开方式。
fopen执行成功返回一个非空的FILE*指针,失败时返回NULL
"r"或"rb":以只读方式打开。

“w”或”wb”:以只写方式打开,并把文件长度截短为零。
“a”或”ab”:以写方式打开,新内容追加在文件尾。
“r+”或”rb+”或”r+b”:以更新方式打开(读和写)。
“w+”或”wb+”或”w+b”:以更新方式打开,并把文件长度截短为零。
“a+”或”ab+”或”a+b”:以更新方式打开,新内容追加在文件尾。

注:字母b表示文件是一个二进制文件而不是文本文件。

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

数据从文件流stream读到ptr指向的数据缓冲区里。size参数指定每个数据记录的长度,nmemb给出要传输的记录的个数。函数的返回值是成功读到数据缓冲区里的记录的个数(不是字节数)。

fwrite()从指定的数据缓冲区里取出数据记录,并把它们写到输出流中。它的返回值是成功写入的记录个数。函数原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);


fputc()把一个字符写到一个输出文件流中,它返回写入的值,如果失败,则返回EOF。类似fgetc()和getc(),putc()的作用也相当于fputc(),但它可能被实现为一个宏。putchar()相当于putc(c, stdout),它把单个字符写到标准输出。
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

注意:putchar和getchar都是把字符当作int类型而不是char类型来使用的,这就允许文件结尾EOF取值为-1。

fgetc()从文件流里取出下一个字节并把它作为一个字符返回。当它到达文件结尾或出现错误时,返回EOF。getc()和fgetc()一样,但它有可能被实现为一个宏。getchar()相当于getc(stdin)。
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

fseek()用于在文件流里为下一次读写操作指定位置。
int fseek(FILE *stream, long offset, int whence);
offset和whence参数的含义和取值与lseek函数完全一样,但lseek返回的是一个off_t数值,而fseek返回的是一个整数:0表示成功,-1表示失败并设置errno指出错误。