目录
回顾C语言IO操作
默认会打开的三个输入输出流
认识系统IO
open
open函数的第一个参数是pathname,表示要打开或创建的目标文件
open函数的第二个参数是flags,表示打开文件的方式
open函数的第三个参数是mode,表示创建文件的默认权限
open的返回值
close
write
read
文件描述符fd
重定向
输出重定向
输入重定向
追加重定向
标准输出流和标准错误流对应的都是显示器,它们有什么区别?
重定向函数dup2
FILE
如何理解缓冲区?
在C语言中 stdin, stdout, stderr
在C++中 cin,cout,cerr
其中
stdin(cin)——标准输入流
stdout(cout)——标准输出流
stderr(cerr)——标准错误流
在Linux中一切皆文件,stdin(cin)对应的就是键盘对应的文件
stdout(cout)和stderr(cerr)对应的就是显示器对应的文件
并且都是FILE*类型
每一种语言都有自己对应的文件操作函数,如打开文件,关闭文件等,但这些接口的本质,还是对系统IO接口的封装
open函数的第一个参数是pathname,表示要打开或创建的目标文件
若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
open函数的第二个参数是flags,表示打开文件的方式
常用选项
open函数的第三个参数是mode,表示创建文件的默认权限
注意:这里的默认权限会受到umask权限掩码的影响(你所看到的权限掩码=mode&(~umask)),所以在使用之前要把umask设置 设置为0
umask(0)
open的返回值为新打开文件的文件描述符fd
从结果可以看到文描述符是从3开始递增的
打开不存在的文件
int close(int fd);
close函数时传入需要关闭文件的文件描述符,关闭文件成功返回0,关闭文件失败返回-1。
ssize_t write(int fd, const void *buf, size_t count);
将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中
例
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
const char* msg = "hello world\n";
for (int i = 0; i < 5; i++){
write(fd, msg, strlen(msg));
}
数据写入成功,返回实际写入数据的字节个数;数据写入失败,返回-1
ssize_t read(int fd, void *buf, size_t count);
从文件描述符为fd的文件读取count字节的数据到buf位置当中
数据读取成功,返回实际读取数据的字节个数;数据读取失败,返回-1
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
关闭文件描述符0
这里直接给出结论:
进程中,文件描述符的分配规则:在文件描述符表中,最小的,没有被使用的数组元素,分配给新文件
原理:在上层无法感知的情况下,在OS内部更改进程对应的文件描述符表中特定下标的指向
输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中
在打开文件之前关闭stdout的文件描述符,然后打开文件,那么这个新打开的文件描述符就是1
而系统默认会打印内容到显示器也就是文件描述符为1的显示器文件中
这里也是一样,事先将数据放要打开的文件中,在关闭0号文描述符后打开文件,键盘文件读取会到log.txt中
追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据
重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。
int dup2(int oldfd, int newfd);
从英文描述上看很容易会认为是将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中
如果要将1文件描述符的内容指向fd指向的文件该怎么用呢?dup2(1,fd)还是dup2(fd,1)
不要认为fd指向的文件是新打开的 就是newfd的位置,这里要看函数本身的用法
答案这里这里的正确用法是 dup(fd,1)
#include
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT|O_APPEND, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello world\n");
return 0;
}
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访 问的。
所以C库当中的FILE结构体内部,必定封装了fd。
如何理解缓冲区?
下面看一个例子
在显示器上打印了两行信息,但是重定向到log.txt中有三行
C语言除了要在struct FILE封装fd 还要在结构体内预留一部分缓冲区,所以当fprintf使用的时候,是将数据先放到了缓冲区中,然后这个函数就会直接返回,接下来C标准库会结合一定的刷新策略将缓冲区中的数据写入给OS(write)
首先我们应该知道的是,缓冲的方式有以下三种:
- 无缓冲。(不提供缓冲,直接给OS)
- 行缓冲。(常见的对显示器进行刷新数据)
- 全缓冲。(常见的对磁盘文件写入数据,缓冲区写满才刷新)
这个缓冲区在哪里?—— 在FILE结构体当中
为什么这里文件里打印了两次fprintf打印了两次?
当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和puts函数打印的数据就有两份。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份