基础IO(2)--文件描述符以及输入输出重定向

文件描述符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:函数指针,会指向硬件的操作方法

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值