Linux 系统里面的文件IO指的是系统调用的文件IO,并不是C标准中的文件IO。
既然已经学习了C标准的文件IO,那么为什么还要学习系统调用的文件IO?
我们进行区分:
32位系统中,一个进程启动之后有4G的虚拟内存。
高1G是内核空间。
低3G是用户空间。
用户空间是每个进程独有。
内核空间是所有进程公用。
C标准文件IO,fopen之后在用户空间创建8K的文件缓冲区。
使用fopen函数、fread函数、fwrite函数、fclose函数的时候都是在用户空间的文件缓冲区做处理。
调用fopen函数,打开文件成功之后,函数返回类型为 FILE 类型的结构体。
FILE 结构体中的buffer指针就指向了用户空间的文件缓冲区。
FILE结构体中还有文件的读写位置偏移量Pos。
系统调用是在用户空间和内核空间之间的一套接口。
系统调用是一套更加底层的接口。
C标准文件IO接口封装了系统调用的文件IO接口。
为什么读写文件的时候C标准文件IO接口一定要封装系统调用的文件IO接口呢?
普通文件存放在磁盘,读写磁盘需要通过磁盘的驱动程序。
磁盘的驱动程序运行在操作系统的内核空间。
所以操作一个文件,本质是操作一个磁盘文件,操作磁盘肯定要经过linux内核,内核中找到对应的磁盘驱动,通过磁盘驱动来读写磁盘数据。所以读写文件的时候C标准文件IO接口一定要封装系统调用的文件IO接口。
操作硬件的过程:
应用程序–>用户空间–>内核空间–>驱动程序–>硬件操作。
系统调用的API是建立起内核空间和用户空间之间的纽带。
系统调用的API一部分运行在内核空间,一部分运行在用户空间。
系统调用的open函数调用成功之后返回一个非负整数的文件描述符。
文件描述符用来表示当前进程打开的文件。文件描述符的值根据当前进程打开文件的顺序,第一个打开的文件,文件描述符为0,第二个打开的文件,文件描述符就是1,依此类推。
系统调用 open函数 返回文件描述符之后,我们就可以使用文件描述符对于特定文件进行读写操作。
C标准的 fopen函数 打开文件的时候调用的就是open函数,C标准文件IO在操作文件的时候也是操作文件描述符,只不过把open函数返回的文件描述符保存在了 FILE结构体 里面进行包装。
为什么要在FILE结构体中添加buffer指针?
读写内存的速度远高于读写磁盘的速度,增加缓存区buffer的目的是为了先把要操作的数据保存在缓冲区,再一次性的通过系统调用把数据写到磁盘里面去,避免频繁重复的操作磁盘文件。
既然有了C标准的文件IO了,为什么还要接触更底层的文件IO呢?
C标准的文件IO只适用于普通文本的读写,但是如果是QQ发消息,发送的是 “你好” ,那么 “你好” 就会缓存到buffer里面,等缓存够了8K之后才刷新到对方的QQ里面,这显然是不合适的,所以就要使用更底层的文件IO来实现。
不仅仅只是在网络编程的时候不能使用C标准的文件IO,在管道的读写,设备文件的读写都不能去使用C标准的接口。
对普通计算机用户来说,文件就是存储在永久性存储器上的一段数据流,通常是可执行程序或者是某种格式的数据。文件放置于文件夹,文件夹放置于某个磁盘分区中,这是从普通计算机用户眼里看到的文件。
但linux操作系统中文件的概念,却远远不局限与此,文件是linux对大多数系统资源访问的接口。linux常见的文件类型:普通文件、目录文件、设备文件、管道文件、套接字和链接文件等等。
在linux中所有的进程,在内核中都有一个对应的结构体来描述这个进程task_struct,也叫做进程管理块PCB(process control block),这个结构体中有一个文件描述符表files_struct,用来保存该进程对应的所有文件描述符。
普通文件:普通计算机用户看到的文件,仅仅是linux文件类型中的一种,我们称之为普通文件,它们通常驻留在磁盘上的某处。
普通文件按照信息存储方式来划分,可以分为文本文件和二进制文件:
文本文件:这类文件以文本的某种编码(比如 ASCII码)形式存储在存储器中,它是以“行”为基础结构的一种信息组织和存储方式。
二进制文件:这类文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们,操作系统能够读懂即可,二进制文件一般是可执行程序、图像、声音等等。
目录文件:主要目的是用于管理和组织系统中的大量文件。它存储一组相关文件的位置、大小等文件有关的信息。
设备文件:linux操作系统把每一个I/O设备都看成一个文件,与普通文件一样处理,这样可以使文件与设备的操作尽可能统一,对I/O设备的使用和一般文件的使用一样而不必了解I/O设备的细节和底层差异。
设备文件分为:块设备文件、字符设备文件、网络设备文件。
一切设备皆文件。
管道文件:主要用于在进程间传递数据,管道文件保存在内存里面而不保存在磁盘。
套接字文件:用于网络上的通信,套接字文件保存在内存里面而不保存在磁盘。
符号链接文件:这个文件包含了另一个文件的路径名。被链接的文件可以是任意文件或目录。
符号链接文件分为软链接和硬链接。
上述是linux丰富的文件类型,包括除了普通文件和目录文件之外的几种“特殊文件”,正是由于这些特殊文件的存在,linux程序员可以按照统一的接口来实现基本文件读写、设备访问、硬盘读写、网络通信、系统终端,甚至内核状态信息的访问等等。无论是哪种类型的文件,linux都把他们看作是无结构的流式文件,把文件的内容看作是一系列有序的字符流。
程序要访问一个文件,首先需要通过一个文件路径名来打开文件,当进程打开一个文件的时候,进程将获得一个非负整数标识,即"文件描述符 file description 。通过文件描述符,可以对文件进行I/O处理。
对文件执行I/O操作,有两种基本方式:
一种是系统调用的I/O方法,
另一种是标准C的文件I/O方法。
系统调用的I/O方法和标准C的I/O方法的区别是:
1、基于标准C的文件操作函数的名字都是以字母“f”开头,而系统调用函数则不用,例如 fopen() 对应于系统调用的 open() ;
2、系统调用I/O方法是更低一级的接口,通常完成相同的任务是,比使用基于标准c的I/O方法需要更多编码的工作量。
3、系统调用直接处理文件描述符,而标准C函数则处理 FILE* 类型的文件句柄。
4、基于标准C的I/O方法其实就是对系统调用方法的封装,标准C的I/O方法使用自动缓冲技术,使程序能减少系统调用,从而提高程序的性能。
5、基于标准C的I/O方法替用户处理有关系统调用的细节,比如系统调用被信号中断的处理等等。
基于标准C的I/O方法显然给程序员提供了极大的方便,但是有些程序却不能使用基于标准C的I/O方法。比如使用缓冲技术使得网络通信陷入困境,因为它将干扰网络通讯所使用的通信协议。考虑到这两种I/O方法的不同,在使用终端或者通过文件交换信息时,通常采用基于标准C的I/O方法。而使用网络或者管道通信时,通常采用系统调用的I/O方法。
inux最常用的文件操作系统调用包括:打开open()、创建文件creat(), 关闭文件close(), 读取文件read(),写入文件write(),移动文件指针lseek(),文件控制fcntl()和文件权限access()等。
通过 open() 和 creat() 系统调用,都可以创建一个并打开一个文件,系统调用 open() 和 creat() 成功时,都会返回一个非负数的文件描述符,使用 close() 函数可以关闭指定的文件描述符的文件。
在使用任何与文件相关的系统调用之前,程序应该包含fcntl.h和unistd.h头文件,它们为最普遍的文件例程提供了函数原型和常数。
#include
#include
#include
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
open()函数传入了3个参数:
第一个参数是一个字符串参数,创建或者打开文件的路径。
第二个参数flags用于指定打开文件的权限。
第三个参数指明了新建文件的访问权限。
当open()调用成功后,它会返回一个新的独一无二的文件描述符。
open函数的参数个数可变,有一个典型的参数个数可变的是printf函数。
man 帮助手册:
第一页是命令行。
第二页是系统调用。
第三页是库函数。
open()函数必须指定打开文件的flags标志来指定打开文件的权限。
其中必须指定标志:
O_RDONLY、
O_WRONLY、
O_RDWR
中的一个,其他标志都是可选的,可以将其与前面的三种标志之一进行或运算以生成最终的标志。
例如:
O_WRONLY | O_APPEND 表示以写和追加的方式打开文件。
O_RDONLY | O_WRONLY | O_APPEND
表示以读、写、追加的方式来打开文件。
int open(const char* pathname, int flags);
函数中可以设置flag设置为 O_WRONLY | O_CREAT 才可以创建文件,否则open函数返回 -1。
使用上面两个参数,flag设置为O_WRONLY | O_CREAT 创建的文件权限是错误的字符串,所以使用三个参数的 open 函数设置创建文件时的权限。
int open(const char* pathname, int flags, mode_t mode);
mode_t mode 是一个int类型八进制数据。
例如设置mode 为 0777。
所以flag使用了 O_CREAT 就一定要在 mode 设置创建的文件权限。
如果文件已经存在也可以使用flag为:O_WRONLY | O_CREAT 的open函数去调用。
如果想要让文件已经存在的时候,open函数调用不成功就需要把flag设置为:O_WRONLY | O_CREAT | O_EXCL。设置之后,就会在检查路径文件是否存在,如果存在就报错返回 -1,如果文件不存在就创建文件并且打开成功。
整型数 flag 有32位。会把每一位作为一种标志,所以 flag 可以表示32种标志。
O_RDONLY 以只读方式打开文件
O_WRONLY 以只读方式打开文件
O_RDWR 以读写方式打开文件
O_APPEND 以追加模式打开文件,在每次写入操作指向之前,
自动将文件指针定位到文件末尾,
并不是只在打开文件的时候定位到文件末尾,
但在网络文件系统进行操作时不一定有用。
O_DIRECTORY 假如参数path那么不是一个目录,那么open将失败。
O_CREAT 如果文件不存在就创建,
使用此选项时需要提供第三个参数mode,
文件的访问权限。
O_EXCL 如果使用了这个标志,
则使用O_CREAT标志来打开一个文件时,
如果文件已经存在,
open将返回失败,
但在网络文件系统进行操作时不一定有用。
O_NOFOLLOW 强制参数pathname所指的文件不能是符号链接。
O_NONBLOCK 打开文件后,
对这个文件描述符的所有的操作都以非阻塞方式进行。
O_NDELAY 和O_NONBLOCK完全一样。
O_SYNC 当把数据写入到这个文件描述符时,
强制立即输出到物理设备。
O_TRUNC 如果打开的文件是一个已经存在的普通文件,
并且指定了可写标志(O_WRONLY、O_RDWR),
那么在打开时就消除原文件的所有内容。
但打开的文件是一个FIFO或者终端设备时 ,
这个标志将不起作用。
在目录 /usr/include/asm-generic/fcntl.h 可以查看到。
open函数代码演示:
#include
#include
#include
int main(int argc, char** argv)
{
int fd;
fd = open("./open_test", O_CREAT|O_WRONLY|O_TRUNC, 0640);
if(fd == -1)
{
printf("open failed fd = %d\n", fd);
}
else
{
printf("./open_test created, fd = [%d]\n", fd);
}
return 0;
}
整数类型的变量fd记录了open()函数的返回值。如果fd等于-1,那么表示open()函数返回失败,否则fd记录了系统返回的所新建并打开的文件描述符。
在vim里面使用shift + K直接跳转到函数对应的man帮助手册。
2 + shift + K
3 + shift + K
在程序退出之前,使用close()来关闭文件。
思考问题:我们前面已经说过,对于当前进程来说,使用非负整数的文件描述符来表示打开的文件。第一个打开的文件其对应的文件描述符为0,那么上面代码中为什么打印出来的文件描述符为3呢?
一个进程默认打开3个文件描述符
STDIN_FILENO 0 标准输入文件描述符
STDOUT_FILENO 1 标准输出文件描述符
STDERR_FILENO 2 标准错误文件描述符
新打开文件返回文件描述符表中未使用的最小文件描述符。
上面进程进行时,三个文件被打开, 0、1、2 文件描述符被占用,所以新文件打开的文件描述符为3。
int creat(const char *pathname, mode_t mode);
使用creat替换掉open函数:
fd = creat("/tmp/open_test", 0640);
creat()函数代码演示:
#include
#include
#include
int main(int argc, char** argv)
{
int fd;
fd = creat("./open_test1", 0640);
if(fd == -1)
{
printf("open failed fd = %d\n", fd);
}
else
{
printf("./open_test created, fd = [%d]\n", fd);
}
return 0;
}
使用close函数来关闭文件
close()函数原型:
int close(int fd);
close()系统调用关闭并释放一个文件描述符。close()成功时返回值等于0,错误时返回-1。但是程序一般不需要检查close系统调用的返回值。除非发生严重的程序错误。
当进程执行完成退出的时候文件描述符会自动close关闭。
但是我们一定要在文件操作完成之后使用close关闭文件。
open()和creat()系统调用成功时,都会返回一个新的文件描述符,当返回失败时,将返回-1。通过errno以及使用perror(), 以及strerror()可以查看错误信息。
#include
void perror(const char *s);
char *strerror(int errnum);
可以在 /usr/include/asm-generic/errno.h
以及 /usr/include/asm-generic/errno-base.h
中去查看errno的具体定义值。
查看目录文件:
查找到头文件:
打开头文件查看错误号对应的详细错误信息:
由于linux内核对物理存储可能会有写延时,所以就算成功的关闭了一个文件,也不能保证数据都被成功写入物理存储。当文件关闭时,对文件系统来说一般不去刷新缓冲区。
如果你要保证数据写入磁盘等物理存储设备中就使用fsync()。fsync()函数原型是:
int fsync(int fd);
用touch命令创建一个文件时,创建权限是0666,而touch进程继承了Shell进程的umask掩码,所以最终的文件权限是0666&022=0644。
touch file123
ls -l file123
-rw-rw-r-- 1 where where 0 9月11 23:48 file123
同样道理,用gcc编译生成一个可执行文件时,创建权限是0777,而最终的文件权限是0777 & 022 = 0755。
当前默认设置最大打开文件个数1024。
ulimit -a
修改默认设置最大打开文件个数为2048, 命令设置的方式不能超过10000,并且只是临时生效,重启失效。
ulimit -n 2048
写文件/etc/security/limits.conf,永久生效, 内容如下:
* soft nofile 65535
* hard nofile 65535
其中,“*”表示所有用户都生效,重启后,在任何地方执行ulimit -n就会显示65535。
文件打开后,我们可以使用read()来进行文件的读操作。这个系统调用的函数原型是:
ssize_t read(int fd, void *buf, size_t count);
三个参数分别是:
fd : 要进行读写操作的文件描述符。
buf :要写入文件内容或读出文件内容的内存地址。
count :要读写的字节数。
read()函数是从文件描述符fd所引用的文件中读取count字节到buf缓冲区中。
如果read()成功读取了数据,就返回所读取的字节数目,否则返回-1。
如果read()读到了文件的结尾或者被一个信号所中断,返回值会小于count。当文件指针已经为于文件结尾,read()操作将返回0。
代码演示:
#include
#include
#include
int main(int argc, char** argv)
{
int fd_read;
char buf[100];
int ret;
/*判断是否传入文件名*/
if(argc < 2)
{
printf("Usage read_test FILENAME\n");
return -1;
}
fd_read = open(argv[1], O_RDONLY);
if(fd_read == -1)
{
perror("open error");
return -1;
}
/*读取数据*/
ret = read(fd_read, buf, sizeof(buf));
printf("read return = [%d]\n", ret);
buf[ret]='\0';
/*如果读出来数据,则打印数据*/
if(ret >= 0)
{
printf("========= buf =========\n");
printf("%s\n", buf);
printf("=======================\n");
}
else
{
perror("read error");
}
close(fd_read);
return 0;
}
运行结果为:
read函数从文件读取到的数据不会在buf后面自动加上 ‘\0’。
所以为了防止越界我们通过: buf[ret]='\0';
来避免字符串越界。
当read()返回-1的时候,errno以及使用perror()可以查看读文件的错误信息。
我们经常需要检查的是EINTR错误,产生这个错误是由于 read()系统调用在读取任何数据前被信号所中断。
发生这个错误的时候,文件指针并没有移动,我们需要重新读取数据。