Linux:基础IO(18+19)+文件描述符

1.文件操作函数

C语言文件操作大全-CSDN博客

1-1.理解文件

1-1-1狭义理解:

• ⽂件在磁盘⾥

• 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的

• 磁盘是外设(即是输出设备也是输⼊设备)

• 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出简称 IO

1-1-2广义理解

Linux下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘......这些都是抽象化的过程)

1-1-3文件操作的认知

(1) 文件大小为0,文件要在磁盘上占据空间,因为除了文件的内容,还有文件的属性
(2) 文件 = 内容 + 属性, 存取,操作,围绕内容+属性展开。

(3) 访问文件,需要先打开文件!进程打开的文件,读文件的操作,本质是:进程读文件的操作

(4) 真正管理文件的是操作系统,通过文件相关的系统调用接口来实现(封装底层OS的文件系统调用),操作系统是硬件,磁盘的管理者。

(5) fopen,fclose...库

封装了底层OS的文件系统,操作系统西药把打开的文件管理起来,先描述再组织。

文件分为

1.内存级(被打开)文件

2. 磁盘级文件

当前进程的cwd是什么?

当前运行程序所在的目录

1-2文件操作函数

1-2-1.写入文件

文件的打开和写入:

(1)snprintf 是 C 标准库中的一个函数,用于将格式化的数据写入字符串。与 sprintf 类似,但 snprintf 允许你指定目标缓冲区的大小,从而防止缓冲区溢出。这在编写安全代码时非常重要。

cpp 复制代码
int snprintf(char *str, size_t size, const char *format, ...);
  • str:目标字符串缓冲区。
  • size:缓冲区的大小,包括终止字符 \0
  • format:格式字符串,类似于 printf 的格式字符串。
  • ...:可变参数列表,表示要插入到格式字符串中的值
  • 成功时,返回写入字符串(不包括终止字符 \0)的字符数。
  • 如果返回值大于或等于 size,表示缓冲区不足以容纳整个格式化的字符串,字符串会被截断。

打开文件log.txt,然后向里面进行写入。

1-2-2.读文件fread

文件结束的判断。feof(fp) 判断文件是否读完了。

读文件fread代码。

buffer,

sizeof(buffer) - 1:读取127个

1:读取一个buffer

fp:文件描述符

如上:./myfile log.txt,

argc = 2,argv[1] = log.txt

1-2-3.输出信息到显示器的方式

C默认打开三个输入输出流文件, 所以就可以直接用。

stdin,标准你输入, stdout,标准输出, stderr,标准错误

问题:为什么程序要帮我们默认打开这三个文件?

程序是数据处理的,打开三个默认的, 给程序提供默认的数据源和数据结果, 程序不打开,那就需要我们自己打开,麻烦。我们可以关掉。

1-2-4.打开文件 方式

以读的方式打开:

1.w:

Truncate:默认要把文件先清空。

与 > log.txt :输出重定向一致, 默认重定向就是以写的方式打开文件

2.a:

追加方式打开文件

本质:不清空文件,追加写入, 与 >> 一致

指针移动函数
fseek:
ftell:

写字符串, 不要写\0,文件里就会多\0,文件打开就会乱码,如果需要写入就需要特殊处理, \0是C语言的规矩, 和文件没关系。

1-2-5:创建一个文件

(1)位图传标记位

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

#define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001
#define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010
#define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100
#define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000

void Print(int flags)
{
    if(flags & ONE_FLAG)
    {
        printf("One!\n");
    }
    if(flags & TWO_FLAG)
    {
        printf("Two\n");
    }
    if(flags & THREE_FLAG)
    {
        printf("Three\n");
    }
    if(flags & FOUR_FLAG)
    {
        printf("Four\n");
    }
}


int main()
{
    Print(ONE_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);
    printf("\n");
    Print(ONE_FLAG | FOUR_FLAG);
    printf("\n");
    return 0;
}

用位图来传标记位的方法,1个比特位传一个标记位。

(2)创建一个文件

文件名

标志位:其实本质就是宏

权限位:新建一个文件必须把权限位带上,默认权限0666

(3)代码:

创建一个文件

O_CREATE:创建

O_WRONLY:只读只写

0666:保证指定权限,不然权限都是乱的

实际上是664,umask权限掩码:会屏蔽一定的权限

加上umask(0),使得最后权限展示不受系统影响,显示就是666了

1-2-5.关闭文件:Close + 向文件里面进行写入: write

open

close(关闭文件)

write(int fd,文件描述符号,fd的返回值,const void *buf写入的buf,size_t count,大小):

向文件进行写入,不能写入\0,,所以不需要+1

1-2-6.文件覆盖写和清空写。

为什么没有清空?

只告诉要创建和写入,没告诉要清空。(没有在创建文件的时候加上O_TRUNC),加上了就会清空了。

O_RDONLY,:创建

O_WRONLY:写入

O_TRUNC: 清空写入

O_APPPEND:追加写入

清空写入和追加写入:

1-2-7文本写入和二进制写入

(1)write可以进行文本写入,也可以进行二进制写入:
(2)系统不关心是文本写入还是二进制写入,所以是void*类型
(3)文本写入vs二进制写入: 代码验证:

二进制写入 --- int a = 12345678,实际上是写入a,以二进制的形式写入,展现出来的是乱码

文本写入 --- a写入到buffer,buffer写入文件,显示1234567

结论:

系统不关心我的写入方式---随便写

fread - 二进制写入

fputc -文本写入

文本写入和二进制写入是语言层的概念

1-2-8.读文件read:从指定的文件描述符去读

结果展示:

推论:由于在OS层面只认文件描述符,我们可以预测,FILE是一个结构体,里面一定封装了文件描述符

2.文件描述符

2-1 代码: 打印stdin,stdout,stderr---fileno:filenum,文件描述符

打出: 0 1 2

理解fd是什么东西?

fd = 3;

n

0 1 2 去哪里了呢?

0 1 2 叫做 标准输入,标准输出,标准错误

FILE:是什么呢?

是c语言提供的一个结构体。typedef XXX{ ;;;;;;}FILE

在操作系统接口层面上,只认fd,即文件描述符

FILE里面一定封装了文件描述符。

fileno->filenum

2-2 跨平台:语言层为什么要做库语言级别的封装呢?

把不同平台直接的差异封装到库lib.so里面, C++和Java对里面包含市面上大多数语言(Windows,Linux,MAC)的封装,调用哪个语言,就把对应的语言的系统调用拿出来用,(裁剪进行条件编译)。

增加语言层的可移植性。

平台不一样,导致不可一致性,因为OS不一样,接口不一样,如C++把所有平台都实现一份(用底层进行封装),用户不用关心系统调用,所以语言层的代码就具有可移植性。

对外部表示的语言接口是一致的。

语言为什么要增加可移植性?

本质是为了让更多人用,不让自己被淘汰。

2-3 什么是文件描述符

struct file 表示被打开的文件, 对文件的管理就变成了对struct file链表的增删查改。

文件打开 -- 内核创建struct file

内容加载到文件缓冲区, 属性初始化到struct file

系统初始化,创建一个文件描述符表:struct files_struct

里面包含一个对象, struct file* fd_array[]指向不同的被打开的文件。

文件描述符的本质就是数组下标。

2-4文件描述符表:struct files_struct

文件描述符的本质就是文件描述符表的数组下标

read调用:

系统调用read函数,通过fd找到对应的struct file,再找到对应的文件缓冲区,从磁盘获得数据弄到缓冲区上, 文件缓冲区再拷贝数据到buffer上,我们就读到了。

对文件内容做任何操作:

都必须把文件加载到文件缓冲区上,也就是磁盘到内存的拷贝。

2-5重定向

重定向的原理:

文件描述符的分配原则:最小的,没有被使用的,作为新的fd给用户

可以把标准输入,输出,错误关掉: close(0) --fd = 0;

close(2) -- fd = 2;

来个例子:

1.close(1) -- 把标准输出关了。

2.创建一个log.txt文件, 把fd = 1给log.txt

3.printf只能通过文件描述符1打出来。关掉stdout,再向fd = 1里面进行写入,变成向log.txt的写入。找不到stdout.

结果是打到了log.txt里,这就是重定向的原理。

2-6 dup操作:更改指针描述符的指针指向

dup2( int oldfd,int newfd):让新的newfd变成就得oldfd,也就是让newfd变成oldfd得拷贝。
newfd 和 oldfd一起指向oldfd.

2-7: 输入重定向:

dup(fd, 0):将0指向fd, 关闭fd, 默认输入的时候就会从fd里面获取数据

本来是从标准输入0读取,现在从打开的文件读了

重定向原理:

输出重定向: >

输入重定向: <

追加重定向: >>

3. 完善一点的自定的shell:增加重定向

< >> >

输出重定向:清除 >

输入重定向 <

追加重定向:追加 >>

在自定义shell里面实现重定向

实现:重定向分析,

左半部分:命令

右半部分:待执行文件

>>追加重定向

重定向:不能让父进程重定向,让子进程进行重定向不会影响父进程

子进程才能够检测重定向情况

void RedirCheck(char cmd[])重定向分析,分类处理

Execute:新增对重定向的情况的处理

cpp 复制代码
#include <iostream>
#include <ctype.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#include <sys/stat.h>
#include <fcntl.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

//#define TrimSpace(CMD,index) do{
//    while(isspace(*(CMD+index)))
//    {
//        index++;
//    }
//}while(0)

int redir = NONE_REDIR;
std::string filename;

// for test
char cwd[1024];
char cwdenv[1024];

// last exit code
int lastcode = 0;

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

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

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    //本来要从配置文件来
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

//command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true:false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

bool CheckAndExecBuiltin()
{
    //如果内键命令做重定向,更改shell的标准输入,输出,错误
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        int fd = -1;
        // 子进程检测重定向情况
        if(redir == INPUT_REDIR)
        {
            fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0) exit(1);
            dup2(fd,0);
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir == APPEND_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else
        {}
        // 进程替换,会影响重定向的结果吗?不影响
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

void TrimSpace(char cmd[], int &end)
{
    while(isspace(cmd[end]))
    {
        end++;
    }
}

void RedirCheck(char cmd[])
{
    redir = NONE_REDIR;
    filename.clear();
    int start = 0;
    int end = strlen(cmd)-1;

    //"ls -a -l >> file.txt" > >> <
    while(end > start)
    {
        if(cmd[end] == '<')
        {
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            redir = INPUT_REDIR;
            filename = cmd+end;
            break;
        }
        else if(cmd[end] == '>')
        {
            if(cmd[end-1] == '>')
            {
                //>>
                cmd[end-1] = 0;
                redir = APPEND_REDIR;
            }
            else
            {
                //>
                redir = OUTPUT_REDIR;
            }
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            filename = cmd+end;
            break;
        }
        else
        {
            end--;
        }
    }
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
        RedirCheck(commandline);

 //       printf("redir: %d, filename: %s\n", redir, filename.c_str());

        // 4. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 5. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();
    }
    //cleanup();
    return 0;
}
相关推荐
fiveym2 小时前
解析Debian 10安装Intel Xeon Silver 4510卡住问题及Debian 11/12支持改进
运维·debian
开利网络2 小时前
从“流量”到“留量”:长效用户运营的底层逻辑
大数据·运维·人工智能·自动化·云计算
天机️灵韵2 小时前
VMware Ubuntu20.04.3 LTS设置NAT模式连接
服务器
嘻哈baby2 小时前
管理100台服务器是什么体验?Python一行代码搞定
运维
十六年开源服务商2 小时前
怎样做好WordPress网站数据分析与运维服务
运维·数据挖掘·数据分析
莫白媛2 小时前
浅谈Linux部分语法(从基础操作到自动化编程的三个层次)
linux·运维·自动化
快解析2 小时前
内网穿透快解析注册后添加配置端口教程
linux·服务器·网络
tianyuanwo2 小时前
Linux密码管理深度解析:passwd与chpasswd的底层机制对比
linux·运维·passwd·chpasswd
violet-lz2 小时前
【Linux】VMware虚拟机中的Ubuntu操作系统主文件夹扩容
linux·运维·ubuntu