open
上一篇系统和文件IO博客中,我们使用过open这个函数接口,那么这个接口里面的参数和返回值又有什么意义呢?和文件IO使用的fopen又有什么区别呢?
根据man手册查询结果来看,其中pathname即为要打开或者创建的目标文件的路径。
flags为打开文件的各种选项类似于文件IO中的只读只写等选项,这里可以传入多个常量使用或运算构成flags。
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
最后这个函数的返回值是一个整型,这个代表如果打开成功则返回文件的描述符,打开失败则返回-1。那么这个返回的文件描述符代表什么意思呢?
系统调用和库函数
在认识文件描述符之前,我们需要先理清两个概念:系统调用和库函数。在C语言中我们打开文件所使用的fopen,fclose等都是C标准库中的函数,被称为库函数。
而现在所使用的open、close等属于系统提供的接口函数,称为系统调用接口,之前在认识操作系统时候博客中,曾经一张如下的图。
这里我们可以看到系统调用接口和库函数是上下级的关系,可以认为库函数在某种程度上是系统调用接口函数的一种封装后的结果。
文件描述符
从open函数的返回值来看,文件描述符的类型是一个整型,当我们的程序运行起来后,变成进程之后,默认情况下,操作系统会帮助我们进程打开三个标准输入输出流:0、1、2:标准输入,标准输出,标准错误。其中0、1、2就是文件描述符。
int fd0 = open("./log0.txt",O_CREAT | O_WRONLY , 0644);
int fd1 = open("./log1.txt",O_CREAT | O_WRONLY , 0644);
int fd2 = open("./log2.txt",O_CREAT | O_WRONLY , 0644);
int fd3 = open("./log3.txt",O_CREAT | O_WRONLY , 0644);
int fd4 = open("./log4.txt",O_CREAT | O_WRONLY , 0644);
int fd5 = open("./log5.txt",O_CREAT | O_WRONLY , 0644);
printf("%d %d %d %d %d %d\n",fd0,fd1,fd2,fd3,fd4,fd5);
printf("stdin->%d\n",stdin->_fileno);
printf("stdout->%d\n",stdout->_fileno);
printf("stderr->%d\n",stderr->_fileno);
运行如上代码我们可以看到这里的文件描述符是从3开始的,这是因为系统已经默认帮我们把0,1,2所对应的标准输入,标准输出,标准错误打开了。
进程和文件的关系一般是1对n的关系,操作系统内一定是打开了多个文件的,因此操作系统必须进行对打开文件的管理,即使一个文件没有被打开,在磁盘上,会占用磁盘空间,即使是空文件,文件有属性,属性也是数据。磁盘文件=文件内容+文件属性
从如下示意图可以看出,在我们打开文件的时候,操作系统会在内存中创建相应的数据结构来描述目标文件,这就是file结构体。表示一个已经打开的文件对象。当进程执行open系统调用时,需要让进程和文件关联起来,因此在进程控制块task_struct中存在指针files,它指向一个files_struct结构体,这个结构体最重要的部分就是包含一个指针数组,数组中每个元素都是指向打开文件的指针,所以本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
文件描述符分配规则
close(0);
int fd0 = open("./log0.txt",O_CREAT | O_WRONLY , 0644);
int fd1 = open("./log1.txt",O_CREAT | O_WRONLY , 0644);
int fd2 = open("./log2.txt",O_CREAT | O_WRONLY , 0644);
int fd3 = open("./log3.txt",O_CREAT | O_WRONLY , 0644);
int fd4 = open("./log4.txt",O_CREAT | O_WRONLY , 0644);
int fd5 = open("./log5.txt",O_CREAT | O_WRONLY , 0644);
printf("%d %d %d %d %d %d\n",fd0,fd1,fd2,fd3,fd4,fd5);
运行如上代码,我们将标准输入流关闭,然后使用open接口打开各个文件,查看文件描述符,这里可以看到这里的文件描述符是从0开始的,由于1和2仍然被标准输出和标准错误使用,因此fd1文件描述符是3。这里可以得出以下结论,文件描述符的分配规则是在files_struct中,找到当前未被使用的最小一个下标,作为新的文件下标。
重定向
close(1);
int fd = open("./log.txt",O_CREAT | O_WRONLY | O_APPEND,0644);
printf("%d\n",fd);
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
运行如上代码我们可以看到,程序将标准输出关闭了,结果本来应该打印到屏幕上的内容不见了,那这些内容去哪了呢?
这里可以看到原本应该打印到屏幕上的内容写入到文件中了,因此这就实现了我们之前所使用过的功能:重定向。因此我们可以这样认为,重定向的本质是将原本要写入的0,1,2文件描述符的内容都写到了对应位置的文件中。
dup2
那么难道我们实现重定向都需要关闭标准输入/输出么?必然没有这么麻烦,我们可以使用dup2的系统调用接口,通过man手册查看如下:
通过描述我们可以得知,这个接口函数的功能是将需要被重定向的内容拷贝到需要重定向到的文件当中
输出重定向
int fd = open("./log.txt",O_WRONLY | O_TRUNC);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);//本来应该显示到显示器的内容写入到文件
printf("hello world\n");
这里可以看到使用dup2可以实现重定向功能,将原本应该打印在显示器上的内容,写入文件中,这里还使用了一个O_TRUNC选项,他的功能是清空原有的内容。
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd为1的文件, 但此时,fd为1的文件下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向
追加重定向
追加重定向只需要在输入重定向的基础上加上 O_APPEND选项即可。
int fd = open("./log.txt",O_WRONLY | O_APPEND);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);//本来应该显示到显示器的内容写入到文件
printf("hello world\n");
输入重定向
int fd = open("./log.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,0);
char buffer[1024];
scanf("%s",buffer);
printf("%s\n",buffer);