对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个现有文件或或创建一个新文件时,内核向进程返回一个文件描述符,当读或写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传递给read或write。
按照惯例,UNIX系统shell使用文件描述符0与进程标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准出错相关联,这是各种shell以及很多应用程序使用的惯例,而与UNIX内核无关,如果不遵守这种惯例,那么很多UNIX应用程序就不能正常工作。
在依从POSIX的应用程序中,幻数0、1、2应当替换成符号常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO.这些常量都定义在头文件<unistd.h>中。
通过上面的图,我们可以认为文件描述符是文件描述符表的数组下标。
open函数可以打开或创建一个文件
pathname是打开或创建文件的名字,flags参数可用来说明此函数的多个选项,用下列一个或多个常量进行“或”运算构成flags参数。
O_RDONLY //只读方式
O_WRONLY //只写方式
O_RDWR //读、写方式
close函数关闭一个文件
其参数是该文件的文件描述符
成功则返回0,失败则返回-1
read函数
调用read函数从打开文件中读数据。
#include<unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
有多种情况可使实际读到的字节数少于要求读的字节数:
读普通文件时,在读到要求字节数之前已经到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将回0。
当从终端设备读时,通常一次最多读一行
当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
当从某些面向记录的设备(例如磁盘)读时,一次最多返回一个记录。
当某一信号草成终端,而已经读了部分数据量时。读操作从文件的当前偏移量出开始,在成功返回之前,该偏移量将增加实际独到的字节数
write函数
调用write函数向打开的文件写数据。
#include<unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes)
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
地址空间
我们先来看以下代码:
来看运行结果:
从结果可以看出,父进程和子进程中的g_val值不同,但地址相同,这是什么原因呢?下面通过一个图来说明:
在上面的进程中,fork创建一个子进程即创建了一个新的PCB,两个进程都有一个地址空间(虚拟地址),通过页表将虚拟地址映射到物理地址。父子进程代码共享,数据独立。
fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父子进程的每个相同的打开描述符共享一个文件表项,假设一个进程有三个不同的打开文件,在从fork返回时,我们有如下结构:
C语言文件操作的底层实现—–FILE结构体
C语言的stdio.h头文件中,定义了用于文件操作的结构体FILE。这样,我们通过fopen返回一个文件指针(指向FILE结构体的指针)来进行文件操作。可以在stdio.h(位于visual studio安装目录下的include文件夹下)头文件中查看FILE结构体的定义,如下:
typedef struct
{
short level; /* fill/empty level of buffer */
unsigned flags; /* File status flags */
char fd; /* File descriptor */
unsigned char hold; /* Ungetc char if no buffer */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
unsigned char *curp; /* Current active pointer */
unsigned istemp; /* Temporary file indicator */
short token; /* Used for validity checking */
}FILE; /* This is the FILE object */