1.C文件接口
1.1 当前路径是什么?
我们之前学习了fopen,当我们用读方式打开一个不存在的文件时,会在当前目录创建所需文件。第一个参数表示文件路径,如果只写一个文件名,没有写路径,默认在当前目录下打开该文件。
如果写了绝对路径或相对路径,就会去对应的路径下打开所需文件。
那么,这里的当前路径,到底是什么意思呢?如果我们只学习了C语言,没有学习操作系统相关的知识,是很难理解的。这里的当前路径,其实是指程序运行后的进程所在的当前工作目录。
如何证明这一点呢?
我们先在当前目录用fopen的"w"创建一个文件,
然后用chdir更改当前目录,
更改当前目录后,在proc目录下查询对应进程标识符的目录,可以看到cwd发生了改变。
1.2 "w"和"a"
w会截短文件的长度为0或创建一个文件。 我们用"w"的方式在当前目录创建文件,每次运行该程序都会覆盖原log.txt的内容,这说明"w"写入文件前会清空文件。就像重定向一样,我们可以利用重定向清空文件,
但我们还有一个问题,strlen("要写入的字符串")要不要加1,要把"\0"写入到文件中吗?
答案是不需要。因为C语言在内存里没办法标定字符串的结束,没有字符串类型,只好用"\0"来标识字符串的结尾,跟文件没有关系,仅仅是C语言的规定,我们把字符串写到文件里,字符串可能被java、python等语言编写的程序或者其他编译器或者文本编辑器读取,文件又没有规定字符串必须用"\0"结尾。所以我们只要把字符串的内容写入文本中,不用写入"\0"。
如果我们要向文件中"追加写",可以用"a"。
2.系统文件I/O
2.1 "比特宏"标识符的实现:
每一个比特位表示不同的功能,用宏来表示不同的比特位,函数体中用if语句判断"标识符&宏"的真假,通过向函数传递单个或多个按位或的宏,从而实现单个或多个不同的功能。
2.2 open
第一个参数pathname,表示要打开或要新建的文件名或文件名的路径(包括相对路径或绝对路径,不写路径默认是文件名),
第二个参数flags表示访问文件的格式,
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_TRUNC:在写之前清空文件,将文件内容的长度截断位0
第三个参数表示创建文件的默认权限.
返回值:打开文件的文件描述符。(详情请往下浏览)
举例:
1.系统默认创建文件的权限只写
2.设置新建文件的权限
我们可以通过umask系统调用,在进程内部设置局部权限掩码,根据"就近原则", 系统的权限掩码就不会起作用.
3. 覆盖写/清空写/追加写
我们通过系统调用write,向进程中打开的文件内写入内容.
覆盖写:不带参数O_APPEND和O_TRUNC
清空写:O_TRUNC
追加写:O_APPEND
其实C语言的库函数中fopen的第二个参数就是对系统调用open的第二个参数的封装,但是fopen返回值是FILE*类型,而open返回的类型是int,这两个类型又有什么联系呢?
3.访问文件的本质
3.1 文件描述符
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
文件描述符 - 维基百科,自由的百科全书 (wikipedia.org)
文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件.
一个进程可以打开多个文件,一个文件可以被多个进程打开,这会不会造成冲突呢?
前一个场景我们通过文件描述表使一个进程管理多个文件,而当一个文件被多个进程打开时,在file结构中会有一个成员变量count(可能是别的变量名),专门统计打开该文件的进程数.
如果一个进程proc1打开file,file中的count会加1,另一个进程proc2打开file,count再加1,以此类推.同理,如果进程proc1要关闭file,只要将自己的文件描叙符表中对应的元素(是一个文件指针)置空,file中的count减1,如果count减到0,就真正关闭文件.这其实是采用了"引用计数"的思想.
3.2 使用文件描述符
根据文件描叙符表中的下标,分别向"标准输出"和"标准错误输出"输出字符串.
通过键盘向"标准输入"输入字符串:
3.3 C库中的标准输出输入
我们知道,C程序运行时,也会默认打开三个标准流,分别是stdin,stdout,stderr .他们的类型都是FILE*,而系统文件I/O,都是通过文件描述符操作文件的,所以FILE必须含有文件描述符.
上面代码中的fileno,就是C库对文件描述符的封装.我们关闭系统的标准输出流1,运行程序,可以发现终端屏幕上什么都没有打印,这是因为我们关闭了系统的标准输出(显示器)接口,而printf打印的字符都是标准输出,我们可以查看一下printf的返回值,
printf返回值是13,说明printf成功读取了13个字符,但由于找不到标准输出流对应的文件描述符,所以无法在屏幕上显示读取成功的字符.
这里有人问,为什么fprintf可以将结果打印到屏幕呢? 因为fprintf是通过标准错误输出流打印到屏幕,关闭的是标准输出流,标准错误输出流并没有关闭.标准输出流和标准错误输出流都是指向屏幕,屏幕被当作一个文件,两个流文件指向同一个硬件,关闭其中一个文件流,是将其文件描述符表中对应下标的文件置空,文件内部的count减1,count减到0,进程才会彻底关闭该硬件(进程无法访问该硬件对应的所有文件).(存疑)
3.4 文件描述符的分配规则
从文件描述符表的下标0处开始,寻找最小的未分配下标(这里的未分配意思就是下标对应的元素,也就是文件指针没有指向任何文件,处在未定义或置空状态),给新打开的文件,这个下标就是新打开文件的"文件描述符"。
简而言之,"找小分小",分配找到的最小未分配下标。
4.文件重定向
根据文件描述符的分配规则,我们可以将原本要输出到屏幕上的字符串输出到文件log.txt中,这其实就是输出重定向,我们来验证一下,
但这种通过"关闭文件描述符,再打开文件"的方式太繁琐,
4.1 dup2
我们可以用系统调用dup2,将文件描述符fd1指向的内容拷贝到另一个文件描述符fd2,当我们调用fd2对文件进行操作时,其实是对fd1指向的文件进行操作,
代码实例:输出重定向原理图:
4.2 追加重定向
同理,追加重定向我们只要将open的第二个参数中的宏"O_TRUNC"换成"O_APPEND"即可,
4.3 输入重定向
还是利用dup2,修改文件描述符指向的内容。
5. 模拟shell实现文件重定向功能
lesson19 · fyehong/Linux_notes - 码云 - 开源中国 (gitee.com)
6.为什么文件描述符1和2都指向屏幕
在程序运行时,我们在屏幕上可能同时看到程序正常运行时打印的信息以及错误信息,如果把错误信息和正常输出的信息混在一起,用一个文件描述符表示,我们将无法区别错误信息。 不分在一起,我们可以通过重定向,将两种信息输出到两个文件中,互不干扰。
如果我们非要将两种信息输出到同一个文件all.log中, 可以将文件描述符2重定向到1,因为">all.log"已经将文件描述符1重定向到文件all.log,所以文件描述符1和2都指向文件all.log。