1. open 接口介绍
使用 man open
指令查看手册:
# 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) ;
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:- 1
open
函数具体使用哪个,和具体应用场景有关。如:目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限;否则使用两个参数的 open。
write
read
close
lseek
,类比 C 文件相关接口。
1.1 代码演示
操作文件,除了使用 C 语言的接口【Linux】回顾 C 文件接口 ,还可以采用系统接口来进行文件访问;
写文件:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
# include <unistd.h>
# include <string.h>
int main ( )
{
umask ( 0 ) ;
int fd = open ( "myfile" , O_WRONLY | O_CREAT, 0644 ) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
int count = 5 ;
const char * msg = "hello open!\n" ;
int len = strlen ( msg) ;
while ( count-- )
{
write ( fd, msg, len) ;
}
close ( fd) ;
return 0 ;
}
读文件:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
# include <unistd.h>
# include <string.h>
int main ( )
{
int fd = open ( "myfile" , O_RDONLY) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
const char * msg = "hello open!\n" ;
char buf[ 1024 ] ;
while ( 1 )
{
ssize_t s = read ( fd, buf, strlen ( msg) ) ;
if ( s > 0 )
{
printf ( "%s" , buf) ;
}
else
{
break ;
}
}
close ( fd) ;
return 0 ;
}
1.2 open 函数返回值
在认识返回值之前,先来认识两个概念:系统调用
和 库函数
:
fopen
fclose
fread
fwrite
都是 C 标准库当中的函数,我们称之为库函数(libc);而 open
close
read
write
lseek
都属于系统提供的接口,称之为系统调用接口;
系统调用接口与库函数的关系如上图; 所以,可以认为,f#
系列的函数,都是对系统调用的封装,方便二次开发。
2. 文件描述符 fd
2.1 0 / 1 / 2
Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0,标准输出 1,标准错误 2 ; 0,1,2 对应的物理设备一般是:键盘,显示器,显示器; 所以输入输出也可以采用如下方式:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
# include <string.h>
int main ( )
{
char buf[ 1024 ] ;
ssize_t s = read ( 0 , buf, sizeof ( buf) ) ;
if ( s > 0 )
{
buf[ s] = 0 ;
write ( 1 , buf, strlen ( buf) ) ;
write ( 2 , buf, strlen ( buf) ) ;
}
return 0 ;
}
现在我们知道,文件描述符就是从 0 开始的小整数; 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了 file 结构体,表示一个已经打开的文件对象; 而进程执行 open 系统调用,就必须让进程和文件关联起来; 每个进程都有一个指针 *files
,指向一张表 files_struct
,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针; 所以,本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就可以找到对应的文件。
2.2 文件描述符的分配规则
直接看代码:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
int main ( )
{
int fd = open ( "myfile" , O_RDONLY) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
printf ( "fd: %d\n" , fd) ;
close ( fd) ;
return 0 ;
}
输出发现是 fd: 3
,
关闭 0 或者 2,再看:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
int main ( )
{
close ( 0 ) ;
int fd = open ( "myfile" , O_RDONLY) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
printf ( "fd: %d\n" , fd) ;
close ( fd) ;
return 0 ;
}
发现结果是:fd: 0
或者 fd: 2
,
可见,文件描述符的分配规则:在 files_struct
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符,会分配给最新打开的文件。
3. 重定向
那如果关闭 1 呢?看代码:
# include <stdio.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <fcntl.h>
# include <stdlib.h>
int main ( )
{
close ( 1 ) ;
int fd = open ( "myfile" , O_WRONLY | O_CREAT, 00644 ) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
printf ( "fd: %d\n" , fd) ;
fflush ( stdout ) ;
close ( fd) ;
exit ( 0 ) ;
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile
当中,其中 fd = 1。这种现象叫做输出重定向。
常见的重定向有:>
,>>
,<
。
那重定向的本质是什么呢?
3.1 dup2 系统调用函数
函数原型如下:
# include <unistd.h>
int dup2 ( int oldfd, int newfd) ;
函数简介:
makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
将newfd设置为oldfd的副本,并在必要时先关闭newfd,但请注意以下事项:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
如果oldfd不是有效的文件描述符,则调用失败,newfd不会关闭。
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2 ( ) does nothing, and returns newfd.
如果oldfd是一个有效的文件描述符,并且newfd与oldfd具有相同的值,那么dup2 ( ) 什么都不做,并返回newfd。
示例代码:
# include <stdio.h>
# include <unistd.h>
# include <fcntl.h>
int main ( )
{
int fd = open ( "./log" , O_CREAT | O_RDWR, 0644 ) ;
if ( fd < 0 )
{
perror ( "open" ) ;
return 1 ;
}
close ( 1 ) ;
dup2 ( fd, 1 ) ;
int i = 0 ;
for ( i = 0 ; i < 5 ; i++ )
{
char buf[ 1024 ] = { 0 } ;
ssize_t read_size = read ( 0 , buf, sizeof ( buf) - 1 ) ;
if ( read_size < 0 )
{
perror ( "read" ) ;
break ;
}
printf ( "%s" , buf) ;
fflush ( stdout ) ;
}
return 0 ;
}
printf 是 C 库当中的 IO 函数,一般往 stdout
中输出,但是 stdout 底层访问文件的时候,找的还是 fd:1
; 但此时 fd:1 下标所表示的内容已经变成了 log 的地址,不再是显示器文件的地址; 所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
4. FILE 与 缓冲区
因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。 所以 C 库当中的 FILE 结构体内部,必定封装了 fd。 缓冲区就是一块内存区域,其存在目的是为了提升使用者的效率(用空间换时间)。 我们这里说的缓冲区是语言层面的缓冲区,也就是 C 自带的缓冲区,跟内核中的缓冲区没有关系。 缓冲区刷新方式:
无缓冲 - 无刷新; 行缓冲 - 行刷新 :写满一行才刷新,我们平时写代码经常会遇到缓冲区的问题; 全缓冲 - 全部刷新:在普通文件中写入时,缓冲区被写满,才刷新! 强制刷新:使用各种方法让缓冲区强制刷新,如:fflush()
函数; 自动刷新:程序退出的时候会自动刷新。
来段代码研究一下:
# include <stdio.h>
# include <string.h>
int main ( )
{
const char * msg0 = "hello printf\n" ;
const char * msg1 = "hello fwrite\n" ;
const char * msg2 = "hello write\n" ;
printf ( "%s" , msg0) ;
fwrite ( msg1, strlen ( msg0) , 1 , stdout ) ;
write ( 1 , msg2, strlen ( msg2) ) ;
fork ( ) ;
return 0 ;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢?./a.out > file
,我们发现结果变成了:
hello write
hello printf
hello fwrite
hello peintf
hello fwrite
我们发现 printf
和 fwrite
(库函数)都输出了 2 次,而 write
只输出了一次(系统调用)。
为什么呢?肯定和 fork 有关:
一般 C 库函数写入文件是全缓冲的,而写入显示器是行缓冲。 printf
fwrite
库函数会自带缓冲区(进度条例子可以说明【Linux】编写第一个小程序:进度条 ),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,即使是 fork 之后; 但是进程退出之后,会统一刷新,写入文件当中。 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。 write
没有变化,说明没有所谓的缓冲。
综上:printf
fwrite
库函数会自带缓冲区,而 write
系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不在我们讨论范围之内。那这个缓冲区谁提供呢?printf
fwrite
是库函数,writre
是系统调用,库函数在系统调用的“上层”,是对系统调用的“封装”,但是 write
没有缓冲区,而 printf
fwrite
有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由 C 标准库提供。
END