Linux:理解IO,重定向

对C的文件操作不了解的可以先看下面这篇博客:

C语言【文件操作】详解中(会使用fgetc,fputc,fgets,fputs,fscanf,fprintf,fread,fwrite函数)_fscanf fgets-CSDN博客

一、Linux下系统级的文件操作

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到

1.一种传标志位的方法

cpp 复制代码
#include <stdio.h>

#define ONE (1 << 0)
#define TWO (1 << 1)
#define THREE (1 << 2)
#define FOUR (1 << 3)

void Print(int flag)
{
    if(flag & ONE)
        printf("one\n");
    if(flag & TWO)
        printf("two\n");
    if(flag & THREE)
        printf("three\n");
    if(flag & FOUR)
        printf("four\n");
}

int main()
{
    Print(ONE);
    printf("\n");
    Print(ONE|TWO);
    printf("\n");
    Print(ONE|TWO|THREE);
    printf("\n");
    Print(ONE|TWO|THREE|FOUR);
    printf("\n");
    Print(ONE|FOUR);
    printf("\n");
    return 0;
}

通过这种方法,就可以让一个函数根据不同的参数实现不同的功能(如:上面的代码中的if语句里面的代码快就可以设计成不同的功能)

2.open接口介绍

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
/* pathname: 要打开或创建的目标文件
 flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 "或"运算,构成flags。
 参数:  O_RDONLY: 只读打开                
 		   O_WRONLY: 只写打开    
        O_RDWR  : 读,写打开                          
这三个常量,必须指定一个且只能指定一个
        O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
		O_APPEND: 追加写
	成功:新打开的文件描述符
	失败:-1
*/

open的本质 是将文件数据(内容加属性)从硬盘拷贝到CUP的内存中(符合冯诺依曼体系)。

open函数具体使用哪个,和具体应用场景相关。

write read close lseek,类比c文件相关的接口

3.open函数的返回值

这里的返回值到底是什么呢??
在介绍这里的返回值时,先回顾一下两个概念:系统调用和库函数

  • fopen fclose fread fwrite 都是C标准库当中的函数,称之为库函数
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口

结论:f# 系列的函数,都是对系统调用的封装,方便二次开发

文件描述符fd

open函数的返回值就是文件描述符, 是一个整数

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    printf("stdin->fd:%d\n", stdin->_fileno);   // 0
    printf("stdout->fd:%d\n", stdout->_fileno); // 1
    printf("stderr->fd:%d\n", stderr->_fileno); // 2

    return 0;
}

文件描述符就是从0开始的小整数。 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件 。于是就有了file结构体 。表示一个已经打开的文件对象 。而进程执行 open 系统调用 ,所以必须让进程和文件关联起来。每个进程都有一个指针 *files指向一张表files_struct, 该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!!

所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。


二、文件描述符的分配规则

先看代码案例:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
    int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fda);
    int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdb);
    int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdc);
    int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdd);
}

运行结果:

bash 复制代码
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:3
fdb:4
fdc:5
fdd:6

关闭0或者2,看一下fd是怎么分配的

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{

  // 0 1 2
    close(0);
    int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fda);
    int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdb);
    int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdc);
    int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdd);

}

运行结果:

bash 复制代码
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:0
fdb:3
fdc:4
fdd:5

关闭2:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{

  // 0 1 2
    close(2);
    int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fda);
    int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdb);
    int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdc);
    int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdd);

}

运行结果:

cpp 复制代码
[zhangsan@hcss-ecs-f571 IO]$ ./operfile1
fda:2
fdb:3
fdc:4
fdd:5

文件描述符的分配规则:

在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

三、重定向的本质

关闭文件描述符1,看一下会有什么效果

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{

  // 0 1 2
    close(1);
    int fda = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fda);
    int fdb = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdb);
    int fdc = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdc);
    int fdd = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);
    printf("fd:%d\n", fdd);

}

运行会发现,打印结果没有打到屏幕上,而是打印到了log1.txt文件中

这个因为log1.txt文件描述符占了1位置,所以内容就被输入到了log1.txt文件中。

重定向的本质:

  1. open函数打开的模式不同
  2. 文件描述符的修改

四、dup2系统调用

cpp 复制代码
#include <unistd.h>

int dup2(int oldfd, int newfd);

作用:将oldfd文件描述符里面的内容拷贝到newfd中

用dup2实现一下输入重定向

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{

    // close(0); // 关闭stdin

    int fd = open("log1.txt", O_RDWR);
    printf("fd:%d\n", fd);
    dup2(fd, 0);
    char buffer[64];
    fgets(buffer, sizeof(buffer), stdin);
    printf("%s\n", buffer);
    // 0 1 2
   // close(1);
}

这段代码是将文件中的内容输入到屏幕上

五、在自主shell中添加重定向功能

原理:

  1. 检查输入的字符串中有无重定向的字符:">" 或者 ">>" 或者 "<"
  2. 若有,则在子进程中调用dup2函数来进行重定向
    若没有,则正常执行代码

实现重定向功能代码:

cpp 复制代码
33 // ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
 34 #define NoneRedir 0
 35 #define InputRedir 1
 36 #define AppRedir 2
 37 #define OutputRedir 3
 38 
 39 int redir_type = NoneRedir; // 记录正在执行的程序,重定向的方式
 40 char* filename = NULL; // 保存重定向的目标文件
 41 
 42 // 这里写成宏函数,就不用传二级指针了,而是直接替换到下面的代码中
 43 // 下面这里是一个编程技巧,可以避免替换后因为有分号的音响
 44 // 也可以把代码快写在{}中
 45 
 46 #define Trimspace(start) do {\
 47     while(isspace(*start))\
 48     ++start;\
 49 } while(0)
cpp 复制代码
119 // ls -a -l >> xx.txt  || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
120 void parseRedir(char commandline[])
121 {
122     redir_type  = NoneRedir;
123     filename = NULL;                                                                                                                                                   
124     char *start = commandline;
125     char *end = commandline + strlen(commandline);
126     while(start < end)
127     {
128         if(*start == '>')
129         {
130             if(*(start + 1) == '>')
131             {
132                 // 追加重定向
133                 *start = '\0';
134                 start++;
135                 *start = '\0';
136                 start++;
137                 Trimspace(start);// 去掉左边的空格
138                 redir_type = AppRedir;
139                 filename = start;
140                 break; 
141             }
142             // 输出重定向
143             *start = '\0';
144             start++;
145             Trimspace(start);
146             redir_type = OutputRedir;
147             filename = start;
148             break;
149         }
150         else if(*start == '<')
151         {
152             // 输入重定向
153             *start = '\0';
154             start++;
155             Trimspace(start);
156             redir_type = InputRedir;
157             filename = start;
158             break; 
159         }
160         else 
161         {
162             // 没有重定向 
163             start++;
164         }
165     }
166 }
cpp 复制代码
239 int ExecuteCommand()
240 {                                                                                                                                                                      
241     // 不能让bash执行程序替换函数,这里需要创建子进程
242     pid_t id = fork();
243     if(id < 0)
244         return -1;
245     else if(id == 0)
246     {
247         // 子进程   如何执行?
248         int fd = -1;
249         if(redir_type == NoneRedir)
250         {
251             // Do Nothing
252         }
253         else if(redir_type == OutputRedir)
254         {
255             // 输出重定向
256             fd = open(filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
257             dup2(fd, 1);
258         }
259         else if(redir_type == AppRedir)
260         {
261             
262             fd = open(filename, O_WRONLY | O_CREAT, 0666);
263             dup2(fd, 1);
264         }
265         else if(redir_type == InputRedir)
266         {
267  
268             fd = open(filename, O_RDONLY, 0666);
269             dup2(fd, 0);
270         }
271         else 
272         {
273             // Bug ??
274         }
275         execvpe(gargv[0], gargv, genv);
276         exit(1); // 程序替换失败,退出码设为1
277     }
278     else 
279     {
280         // 父进程等子进程
281         int status = 0;
282         pid_t rid = waitpid(id, &status, 0);
283         if(rid > 0)
284         {
285             // 等待成功
286             // printf("wait child process success!\n");
287             lastcode = WEXITSTATUS(status); 
288         }
289     }
290     return 0;
291 }

自主shell实现的所有代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <iostream>
#include <ctype.h>
#include <string>
#define MAXSIZE 128
#define MAXARGS 32

// 传说中的命令行参数表
// shell自己内部维护的第一张表
// 命令行参数表
char* gargv[MAXARGS];
int gargc = 0;

// 环境变量表
char* genv[MAXSIZE];
int genvc = 0;

// 分隔符
const char* gsep = " ";
// shell自己所处的工作路径
char cwd[MAXSIZE];

//最近一个命令执行完毕,退出码
int lastcode = 0;

// ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
#define NoneRedir 0
#define InputRedir 1
#define AppRedir 2
#define OutputRedir 3

int redir_type = NoneRedir; // 记录正在执行的程序,重定向的方式
char* filename = NULL; // 保存重定向的目标文件

// 这里写成宏函数,就不用传二级指针了,而是直接替换到下面的代码中
// 下面这里是一个编程技巧,可以避免替换后因为有分号的音响
// 也可以把代码快写在{}中

#define Trimspace(start) do {\
    while(isspace(*start))\
    ++start;\
} while(0)
void LoadEnv()
{
    // 正常情况下,环境变量表内部是从环境变量里来的
    // 这里从父进程中取
    extern char **environ;
    
    for(;environ[genvc]; genvc++)
    {
         genv[genvc] = (char*)malloc(sizeof(char)*4096);
        // genv[genvc] = new char[4096];
        strcpy(genv[genvc], environ[genvc]);// 这里必须拷贝过来,不能指向environ里面的地址处
    }
    genv[genvc] = NULL;

}

static std::string rfindDir(const std::string& p)
{
    if(p == "/") 
        return p;
    const std::string psep = "/";
    auto pos = p.rfind(psep);
    if(pos == std::string::npos)
        return std::string();
    return p.substr(pos + 1);
}

const char* GetUserName()
{
    char* name = getenv("USER");
    if(name == NULL)
        return "None";
    return name;
}

const char* GetHostName()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname == NULL)  
        return "None";
    return hostname;
}

const char* GetPwd()
{
    //char* pwd = getenv("PWD");
    char* pwd = getcwd(cwd, sizeof cwd); // 这里是绝对路径  /home/zhangsan
    if(pwd == NULL)
        return "None";
    return pwd;
}

void PrintCommanLine()
{
    printf("[%s@%s %s]# ",GetUserName(), GetHostName(),rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
    fflush(stdout);

}

int GetCommand(char commandline[], int size)
{

    if(NULL == fgets(commandline, size, stdin))
        return 0;
    //2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
        commandline[strlen(commandline)-1] = '\0';
        return strlen(commandline);
}

// ls -a -l >> xx.txt  || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
void parseRedir(char commandline[])
{
    redir_type  = NoneRedir;
    filename = NULL;
    char *start = commandline;
    char *end = commandline + strlen(commandline);
    while(start < end)
    {
        if(*start == '>')
        {
            if(*(start + 1) == '>')
            {
                // 追加重定向
        	    *start = '\0';
                start++;
                *start = '\0';
                start++;
                Trimspace(start);// 去掉左边的空格
                redir_type = AppRedir;
                filename = start;
                break; 
            }
            // 输出重定向
            *start = '\0';
            start++;
            Trimspace(start);
            redir_type = OutputRedir;
            filename = start;
            break;
        }
        else if(*start == '<')
        {
            // 输入重定向
            *start = '\0';
            start++;
            Trimspace(start);
            redir_type = InputRedir;
            filename = start;
            break; 
        }
        else 
        {
            // 没有重定向 
            start++;
        }
    }
}
int ParseCommand(char commandline[])
{

    // 每次进来清空gargv表
    gargc = 0;
    memset(gargv, 0, sizeof gargv);

    // 故意写成= 
    // 单个命令也没有错误,while语句里面会加1的 
    gargv[0] = strtok(commandline, gsep);
    while((gargv[++gargc] = strtok(NULL, gsep)));
   // printf("gargc:%d\n", gargc);
   // int i = 0;
   // for(;gargv[i]; i++)
   // {
   //     printf("gargv[%d]:%s\n",i,gargv[i]);
   // }
    return gargc;
}
// return val:
// 0 :不是内建命令
// 1 :是内建命令 && 执行完毕(没有风险的)
int CheckBuiltinExecute()
{

    if(strcmp(gargv[0],"cd") == 0 )
    {
        // 内建命令
        if(gargc == 2)
        {
            // 新的目标路径:gargv[1]
            // 1.更改进程内核中的路径
            chdir(gargv[1]);
            // 2.更改环境变量
            char pwd[1024];
            getcwd(pwd, sizeof(pwd));
            // 这里避免"PWD="被覆盖(因为putenv不会拷贝字符串,而是直接使用传入指针指向的内存)
            // 需要开辟新的空间,这里加1是因为字符串需要以"\0"结尾。
            char* new_pwd = (char*)malloc(strlen("PWD=") + strlen(pwd) + 1);
            snprintf(new_pwd, strlen("PWD=") + strlen(pwd) + 1, "PWD=%s", pwd);// PWD=/home/zhangsan
            putenv(new_pwd);// 导出环境变量
            // 修改自己的环境变量
            genv[genvc++] = new_pwd;
            genv[genvc] = NULL;
            lastcode = 0;
        }
        return 1;
    }
    else if(strcmp(gargv[0], "echo") == 0) // cd, echo, env, export ... 内建命令
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                // $?    ? -> 看做一个变量名
                if(strcmp(gargv[1] + 1, "?") == 0)
                {
                    printf("lastcode:%d\n", lastcode);
                }
                else if(strcmp(gargv[1] + 1, "PATH") == 0)
                {
                     // 其他环境变量名可以自己添加
                     // 不用 getenv和putenv,可以直接在自己的genv中查找 
                    printf("%s\n", getenv("PATH"));
                }
                lastcode = 0;
             }
         }
        return 1;
    }
    return 0;
}
int ExecuteCommand()
{
    // 不能让bash执行程序替换函数,这里需要创建子进程
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        // 子进程   如何执行?
        int fd = -1;
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 输出重定向
            fd = open(filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
            
            fd = open(filename, O_WRONLY | O_CREAT, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
 
            fd = open(filename, O_RDONLY, 0666);
            dup2(fd, 0);
        }
        else 
        {
            // Bug ??
        }
        execvpe(gargv[0], gargv, genv);
        exit(1); // 程序替换失败,退出码设为1
    }
    else 
    {
        // 父进程等子进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            // 等待成功
            // printf("wait child process success!\n");
            lastcode = WEXITSTATUS(status); 
        }
    }
    return 0;
}

    

int main()
{
    // 0.从配置文件中获取环境变量表,填充环境变量表
    // 这里从父进程里面取
    LoadEnv();
    char command_line[MAXSIZE] = {0};
    while(1)
    {
        //1.打印命令行字符串
        PrintCommanLine();
        //2.获取用户输入    
       if(0 == GetCommand(command_line, sizeof(command_line)))
            continue;
        // printf("%s\n", command_line);
      
        
        // 解析执行重定向的命令  
        // ls -a -l >> xx.txt  || ls -a -l >> xx.txt || cat < log.txt || ls -a -l
       // ls -a -l -> "ls -a -l" && "xxx.txt" && 重定向的方式
       parseRedir(command_line); 
        printf("command:%s\n", command_line);
        printf("redir_type:%d\n", redir_type);
        printf("filename:%s\n", filename);
       //3.解析字符串"ls -a -l" -> "ls" "-a" "-l" 
        //命令行解释器,需要对用户输入的字符串首先进行解析
        ParseCommand(command_line);
        //3.5 判断:这个命令是让父进程bash执行(内建命令),还是让子进程执行
        if(CheckBuiltinExecute() > 0)
        {
                lastcode = 0;
            
            // 是内建命令
            continue;
        }
        //4.子进程执行这个命令    
        ExecuteCommand();
    }
    return 0;
}
相关推荐
Murphy_3121 小时前
从根上了解一下复指数
算法
末日汐21 小时前
linux--进程学习
linux·运维·服务器·学习
你撅嘴真丑21 小时前
素数对 与 不吉利日期
算法
多米Domi01121 小时前
0x3f 第20天 三更24-32 hot100子串
java·python·算法·leetcode·动态规划
胡萝卜的兔21 小时前
ubuntu安装,使用
linux·运维·ubuntu
海盗儿21 小时前
(一)TensorRT-LLM 初探(version: 1.0.0)
linux·运维·windows
wzfj1234521 小时前
Opaque Pointer / Incomplete Type
c++·算法·c
冰西瓜60021 小时前
贪心(四)——拟阵 算法设计与分析 国科大
算法·贪心算法
阿拉伯柠檬21 小时前
传输层协议TCP(二)
linux·服务器·网络·网络协议·tcp/ip·面试