文件描述符fd
文件操作的本质是进程和被打开文件的关系。
进程可以打开多个文件,这些被打开的文件由OS管理,所以操作系统必定要为文件创建对应的内核数据结构标识文件–struct file{}
【与C语言的FILE无关】
通过如下程序
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME(num) "log"#num".txt"
#define FILE_NAME(num) "log"#num".txt"
#define FILE_NAME(num) "log"#num".txt"
#define FILE_NAME(num) "log"#num".txt"
#define FILE_NAME(num) "log"#num".txt"
int main()
{
umask(0);
//以 "w"模式 打开文件
int fd0 = open(FILE_NAME(0), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_TRUNC, 0666);
//打开失败
if(fd0 < 0 || fd1 < 0 | fd2 < 0 || fd3 < 0 || fd4 < 0)
{
perror("open");
return -1;
}
//为什么文件描述符从3开始标号?0 1 2呢?
printf("%d\n", fd0);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
//关闭文件
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
运行得到的结果我们发现,fd从3开始连续编号。那0、1、2呢?答:标准输入输出流。Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
而连续的小整数我们可以想到数组下标!!所以我们猜测进程和文件之间的关系是用数组来描述。
1、以前我们学过三个标准输入输出流
stdin --> 键盘 --> fd:0
stdout --> 显示器 --> fd:1
stderr --> 显示器 --> fd:2
系统中的声明是extern FILE* stdin;
extern FILE* stdout;
extern FILE* stderr;
2、C语言的FILE是结构体变量,是typedef struct _iobuf FILE;
,因为系统接口只认文件描述符,而C库中的f*函数是封装了系统接口来对文件进行操作,所以struct _iobuf {}
结构体里必定有一个字段_fileno
是文件描述符fd。由此可知键盘/显示器也是当作文件来处理。
每个进程的PCB里都有1个struct files_struct *file
指针,指向struct files_struct
结构体,结构体中有1个指针数组struct file* fd_array[]
,指向对应的文件结构体 struct file{}
,进程和文件就这么联系起来了!
每个进程创建的时候,默认都会把stdin/stdout/stderr放进去,故他们占据了进程的文件描述表的0/1/2下标!
fd分配规则
关闭0stdin
和2stderr
,再创建文件,可以看到新创建的文件就占用了0或2文件描述符位置。
规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
关闭1stdout
,再新建myfile.txt
,这个文件就会占据fd=1的位置。本来应该输出到显示器上的内容,就输出到了文件myfile
当中。这种现象叫做输出重定向。常见的重定向有:>, >>, <。
重定向的本质就是上层用的fd不变,在内核中更改fd对应的struct file*
的地址。
dup2
duplicate a file descriptor。系统提供dup函数,其中我们使用dup2函数(在内核中实现文件描述符的拷贝)
#include <unistd.h>
int dup2(int oldfd, int newfd);//把根据oldfd这个下标在文件描述表中的位置所指向的内容struct file{}拷贝给newfd
//让本来要写入newfd的,变成写到oldfd去,返回值是oldfd
--------
dup2(fd0, 1);
就是把fd0指向的struct file{log.txt}拷贝给1(原本1所指向的内容是struct file{stdout}),而上层在调用时还是用fd=1这个传,那内容就会给->1->struct file{log.txt}
那么输出重定向>
的核心代码,以libc的"w"方式打开文件
int fd0 = open(FILE_NAME(0), O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd0, 1);
追加重定向>>
核心代码,把O_TRUNC
改成O_APPEND
即可,以libc的"a"方式打开文件
int fd0 = open(FILE_NAME(0), O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd0, 1);
输入重定向<
,重写输入重定向时,该文件必须存在,以只读方式打开
int fd0 = open(FILE_NAME(0), O_RDONLY);
dup2(0, fd0);
简易shell支持输入输出重定向
代码逻辑:读取用户输入->判断重定向,若是则保存重定向信息->命令空格分割,保存命令及参数->创建子进程->根据重定向信息打开文件,进行重定向->execvp进程程序替换
//标定3种重定向
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define trimSpace(start) do{\
while(isspace(*start)) ++start;\
}while(0)//不带;号
--------------------
int redirType = NONE_REDIR;//默认无重定向
char* redirFile = NULL;//无重定向目标文件
// < > >>
void commandCheck(char* commands)
{
//倒着扫描 效率高些 因为命令和参数比较多
assert(commands);
char* start = commands;
char* end = commands + strlen(commands) - 1;//指向最后一个有效元素
while(end >= start)
{
if(*end == '>')
{
char* filename = end + 1;
*end = '\0';
//判断重定向类型
end--;
//"ls -l >> log.txt"
if(*end == '>')
{
//追加重定向
*end = '\0';
redirType = APPEND_REDIR;
}
//"ls -l > log.txt"
else
{
//输出重定向
redirType = OUTPUT_REDIR;
}
//提取文件名
trimSpace(filename);
redirFile = filename;
break;
}
else if(*end == '<')
{
char* filename = end + 1;
*end = '\0';//拆成左右两部分
trimSpace(filename);//过滤右边的空格,提取文件名
//填写重定向信息
redirType = INPUT_REDIR;
redirFile = filename;
break;
}
else
{
end--;
}
}
}
--------------------
if(id == 0)
{
//命令是子进程执行的,重定向的工作也要由子进程完成
//如何重定向,选项 由父进程给子进程
switch(redirType)
{
case NONE_REDIR:
//什么都不做
break;
case INPUT_REDIR:
{
int fd = open(redirFile, O_RDONLY);
if(fd < 0)
{
perror("open:");
exit(errno);//打开文件失败之间终止子进程
}
//重定向文件打开成功
dup2(fd, 0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
umask(0);
int flags = O_WRONLY | O_CREAT;
//根据输出重定向 或 追加重定向 写好flags
if(redirType == APPEND_REDIR) flags |= O_APPEND;
else flags |= O_TRUNC;
int fd = open(redirFile, flags, 0666);
if(fd < 0)
{
perror("open:");
exit(errno);//打开文件失败之间终止子进程
}
//重定向文件打开成功
dup2(fd, 1);
}
break;
default:
printf("bug?\n");
break;
}
execvp(myargv[0], myargv);
exit(1);
}
问题1:子进程在进行重定向操作时,是否会影响父进程的stdin或stdout?
不会,因为进程具有独立性,父进程PCB中的struct files_struct*所指向的file_struct会有一份拷贝,给子进程。
files struct:子进程会拥有1份独立的文件描述符表(父进程文件表的拷贝)! 这个结构体是属于进程的。
struct file:是父子进程共享的,一份就行。这个结构体是属于文件系统的。
子进程只拷贝文件描述符表,不会拷贝文件。
问题2:在程序替换时(也涉及文件替换),会不会影响曾经进程打开的重定向文件吗?
不会,因为这些数据都属于由操作系统维护的内核数据结构,在进行进程程序替换的时候替换的是代码和数据,而不是struct files_struct*的内容。
更进一步理解一切皆文件
我们有键盘keyboard、显示器tty、硬盘disk、网卡netcard等硬件,每种硬件的访问方法一定是不一样的!而硬件的信息和操作方法是写在各自的驱动上,比如sturct keyboard{ int KeyboardRead(){}//读方法};
等。
在系统层面,文件系统对应的每个struct file
中含有各种文件属性(根据硬件进行调整),每个struct file
挨个链起来,比如有函数指针int (*readp) ();
、int (*writep) ();
,这些函数指针就会指向对应硬件的读方法int KeyboardRead(){}
和写方法,。
**上层调用不同的文件,底层调用不同的方法。**所以我们(在struct file上层来看,所有的文件和设备都是struct file
)在进行读写操作时,我们不关心硬件到底怎么做,操作系统会去调用指针完成对应的操作。
以上就是多态,一个基类多个子类 。
文件系统:fs.h文件中,struct file{};
中有以下参数
struct file{
//...
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct file_operations *f_op;
};
f_count
:是文件的计数引用。打开1.txt
,那么文件1的对应的count++
,关闭1.txt
,则count--
,我们常说的关闭一个文件,其实并不是真的关闭了,只有操作系统看到该文件的count==0
时才会关闭这个文件。比如父子进程同时打开1个文件,子进程close该文件,只是告诉OS我不用这个文件了,实际上count不为0,所以这个文件还是被打开的,父进程不会受影响,关不关这个文件是OS来决定的。
f_flags
:理解为简易shell代码中的这个操作int flags = O_WRONLY | O_CREAT;
f_mode
:文件的权限。
f_pos
:文件的操作位置。为什么追加就是在末尾追加,因为O_APPEND帮我们选好了末尾的操作位置。读写操作都可以让用户决定操作位置。
f_owner
:表示这个文件当前是被谁打开的。
f_op
:函数指针,会指向硬件的操作方法