open一个文件
在Linux中,一个进程启动后,会在内核空间创建一个PCB进程控制块,这个PCB中有一个已打开文件描述符表,记录着所有该进程打开的文件描述符和对应的file结构体地址。默认情况下, 一个进程启动后,会打开三个文件,分别是标准输入、标准输出、标准错误,它们分别使用了0、1、2号文件描述符。
当进程使用open函数打开一个新的文件时,一般会在内核空间申请一个file结构体,并把3号文件描述符对应的file指针执行file结构体,代码如下:
#include <fcntl.h>
#include <stdio.h>
void main() {
int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
printf("new fd = %d\n", fd);
}
输出:
new fd = 3
Process finished with exit code 11
原理图如下:
image.png
process table entry就是进程的文件描述符表,file table entry用来记录文件的读写打开模式,当前文件偏移量以及v-node指针等, v-node table entry是虚拟文件系统对应的文件节点,inode是磁盘文件系统对应的文件节点,通过这两个节点就能找到最终的磁盘文件。每个进程只有一个process table entry, 默认情况下fd0,fd1,fd2已经使用,新打开的文件log.txt将使用fd3。
两个进程同时open一个文件
原理图:
image.png
两个进程对应两个文件描述符表,打开同一个文件时,都各自申请一个file table,由于打开的是同一个文件,所以file table都指向了同一个v-node, 两个file table 如何证明呢?
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
printf("new fd = %d\n", fd);
printf("%ld\n", lseek(fd, 0, SEEK_CUR));
write(fd, "123", 3);
sleep(10);
printf("%ld\n", lseek(fd, 0, SEEK_CUR));
close(fd);
}
输出:
new fd = 3
0
3
#在10秒时间内,启动进程2
输出:
new fd = 3
0
3
两个进程都分配了fd3给新打开的文件,并且读写位置不受其他进程的影响,读写位置正式file table entry的一个属性, 说明是不同的file table entry。
一个进程open 两次同一个文件
一个进程open两次同一个文件,其实跟两个进程open一次的原理相同,都是调用了两次open,反正只要记住,调用一次open函数,就会创建一个file table entry。
image.png
由于只有一个进程,所以只有一个process table entry,open了两次,所以是两个file table entry ,fd 3与fd 4的file pointer指向这两个结构体。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd0 = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
int fd1 = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
printf("new fd0 = %d\n", fd0);
printf("new fd1 = %d\n", fd1);
write(fd0, "123", 3);
printf("fd0 lseek %ld\n", lseek(fd0, 0, SEEK_CUR));
printf("fd1 lseek %ld\n", lseek(fd1, 0, SEEK_CUR));
close(fd0);
close(fd1);
}
输出:
new fd0 = 3
new fd1 = 4
fd0 lseek 3
fd1 lseek 0
上面代码open了两次log.txt,创建了两个file结构体,验证方法还是通过判断读写位置是否是独立的。从输出来看,修改fd0的读写位置不会影响fd1的读写位置。
使用dup复制文件描述符
dup函数与open函数不同,open函数会创建一个file table,但是dup只是申请一个fd来指向一个已经存在的file table。原理图如下:
image.png
//
// Created by wusong on 29/11/18.
//
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd, copyfd;
fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
/*复制fd*/
copyfd = dup(fd);
printf("copyfd %d\n", copyfd);
write(fd, "123", 3);
/*打印出fd和copyfd的偏移量,经过上面的写操作,都变成3了*/
printf("fd lseek %ld\n", lseek(fd, 0, SEEK_CUR));
printf("copyfd lseek %ld\n", lseek(copyfd, 0, SEEK_CUR));
close(fd);
close(copyfd);
return 0;
}
输出:
copyfd 4
fd lseek 3
copyfd lseek 3
结果证明只要操作了fd 或copyfd这两个文件描述符中一个的读写位置,就会影响到另一个文件描述符的读写位置。说明这两个文件描述符指向的是同一个file table。
fork之后open
如果在调用fork之后调用一次open函数,由于fork之后会返回两次,一次父进程返回,一次子进程返回,那么这个时候其实是相当与两个进程分别调用了一次open函数打开同一个文件
image.png
//
// Created by wusong on 29/11/18.
//
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int pid = fork();
int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
write(fd, "123", 3);
sleep(5);
printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
close(fd);
}
输出
pid 9385 0 # 父进程
pid 0 0 # 子进程
pid 0 3
pid 9385 3
可以看到父子进程的读写位置都是3,并不受影响。
fork之前open
fork之前调用open函数,也就是只调用了一次,产生了一个fd以及file table, fork之后子进程的文件描述符表示从父进程复制过来,子进程的fd指向和父进程相同的file table。
原理图如下:
image.png
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd = open("./log.txt", O_RDWR);
int pid = fork();
printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
write(fd, "123", 3);
sleep(5);
printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
close(fd);
}
输出:
pid 9546 0
pid 0 3
pid 0 6
pid 9546 6
父子进程都各自写入3字节,如果是两个file table,那么最终都应该打印的是3,而不是6。此外,如果想要释放这个file table,也必须父子进程都close一次fd才会释放,如果不close,进程退出的时候会自动close掉所有的文件描述符。
重定向的本质
为什么 >/dev/null 2>&1 和 2>&1 >/dev/null效果不同呢?由上面分析我们知道,进程的文件描述符表(File Descriptor Table)将一个文件描述符数字对应到一个文件表(File Table)中的一个文件项,在通过文件表将一个文件项对应到磁盘上具体的node(或者是vnode再到node),重定向本质上就是通过修改文件描述符表中的文件指针,将一个描述符的操作落到其他文件上去,比如3>&1就是将描述符3的文件指针指向和描述符0相同的地方。这个操作在 Linux 系统是通过一个叫 dup2(2) 的系统调用完成的, 2>&1 在程序实现上就是
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
dup2(1, 2);
fprintf(stderr, "hello, worldn");
return 0;
}
我们再来看看 2>&1 >/dev/null 和 >/dev/null 2>&1 的实现,
int fd = open("/dev/null", "w+");
dup2(fd, 1);
dup2(1, 2);
以上就是 >/dev/null 2>&1 的实现。我们先把描述符 1 指向和 /dev/null 的位置,然后再把描述符 2 指向描述符 1 对应文件的位置。由于之前描述符 1 的对应文件已经被 dup2(2) 修改为了 /dev/null 因此描述符 2 现在也指向 /dev/null 了。后续的所有标准输出与标准错误的数据都被定向到了 /dev/null .
再来看 2>&1 >/dev/null 的实现
int fd = open("/dev/null", "w+");
dup2(1, 2);
dup2(fd, 1);
这里仅仅就是把两条 dup2(2) 语句的顺序调换了一下。但是却产生了完全不同的结果。首先,我们把描述符 2 指向描述符 1 对应的文件,也就是标准输出,然后再把描述符 1 指向 /dev/null 。结果是描述符 1 指向了 /dev/null 而描述符 2 仍然指向标准输出。
网友评论