深入解析Linux系统中的“一切皆文件“

0 实现 MiniShell

**我们观察在 shell 中输入命令 和上次文章所讲述的知识,可以写一个简易的 shell **
要写一个 shell 需要循环以下过程

  1. 获取命令行
  2. 解析命令行
  3. 用 fork 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait / waitpid)

代码实现

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SIZE 256
#define NUM 16
// 自己实现了一个命令行解析器:MiniShell
// 需要的知识:1、多进程编程 2、程序替换 3、C字符串操作函数 4、进程的理解
int main()
{
    
    
  // 对输入命令行字符串的解析
  char cmd[SIZE];
  const char *cmd_line = "[temp@腾讯云 MyMiniShell]# ";
  while (1)
  {
    
    
    cmd[0] = 0;
    printf("%s",cmd_line);
    fgets(cmd,SIZE,stdin);
    cmd[strlen(cmd) - 1] = '\0';//为了避免把命令的换行读进去
    char *args[NUM];           // 字符串数组  保存解析后的命令行参数,
    args[0] = strtok(cmd," ");
    int i = 1;
    do {
    
    
       args[i] = strtok(NULL," ");
       if (args[i] == NULL) break;
       ++i;
    }while(1);
    int size = sizeof(args)/sizeof(args[0]);
    args[size - 1] = NULL;

    //多进程和程序替换
    pid_t id = fork();
    if (id < 0) {
    
    
      perror("fork error!\n");
      continue;
    }
    if (id == 0) {
    
    
      // child process
      execvp(args[0],args);
      exit(1);
    }
    int status = 0;
    pid_t ret = waitpid(id,&status,0);
    if (ret > 0) {
    
    
      printf("status code : %d\n",(status >> 8) & 0xff);//这是进程退出码的底层实现,当然也可以用宏
    }else {
    
    
      printf("process wait error!\n");
    }
  }
  return 0;
}

结果呈现
在这里插入图片描述

1 C文件IO

  • C默认会打开三个输入输出流,分别是stdin ,stdout ,stderr
  • 这三个流的类型都是 FILE* 文件指针
    请参考:https://blog.csdn.net/CZHLNN/article/details/110238501

2 文件相关系统调用接口

操作文件,除了上述 C 接口,我们可以使用系统调用接口 open close read write ,C++,java或者其他语言也有类似的IO接口,其底层实现都是系统调用接口。

2.1 open 接口介绍

在这里插入图片描述
pathname:要打开或者创建的目标文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags
参数
O_RDONLY : 只读打开
O_WRONLY:只写打开
O_RDWR :读写打开
这前3个常量,必须指定一个且只能指定一个
O_CREAT :若文件不存在,则创建它,需要使用 mode 选项,来指明新文件的访问权限
O_APPEND : 追加写
返回值
打开文件成功:打开或者创建的文件描述符
打开文件失败:-1

使用
在这里插入图片描述

3 文件描述符

3.1 什么是文件描述符

文件描述符其实是正数,当每次调用 open 打开新建文件,返回的 fd 总是从3开始,那么为什么会这样呢?
其实Linux进程默认会有三个缺省打开的文件描述符,分别是stdin(标准输入)0 ,stdout(标准输出)1,stderr(标准错误)2 ,0,1,2对应的物理设备一般是:键盘、显示器、显示器。那么我们打开文件的时候底层到底做了什么呢?
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,所以有了file 结构体,表示一个已经打开的文件对象,而进程执行open 系统调用,所以必须让进程和文件关联起来,每个进程的pcb里都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个结构体指针数组,每个指针都是指向所打开的文件结构体,文件结构体中有函数指针,函数指针会找到对应的驱动程序,然后驱动会找到硬盘或者显示屏。

我们来看一下Linux内核的源码

在这里插入图片描述
同样的系统调用接口在不同的文件结构体中指向了不同的硬件驱动程序,这其实是OOP中的多态,写OS的大佬用C实现了多态
总结:如图
在这里插入图片描述

3.2 文件描述符的分配规则

这里我们做一下小实验吧,看如下的代码
在这里插入图片描述
fd为

在这里插入图片描述

关闭0 和 2再看
在这里插入图片描述
结果是
在这里插入图片描述
在这里插入图片描述
结果是
在这里插入图片描述
可见,文件描述符的分配规则是:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

3 你是怎么理解重定向的(从OS的角度)

看如下的代码:
在这里插入图片描述
结果是
在这里插入图片描述

此时本来应该输出到显示器上的内容,输出到了文件myfile1中,且 fd = 1。这种现象叫输出重定向。常见的重定向还有 > , >>,<
那底层都干了些什么呢?

在这里插入图片描述
有了上面的知识储备,那么理解这个图应该不难

看下面代码

在这里插入图片描述

printfC库当中的函数,一般往 stdout 中输出,但是 stdout 访问底层文件的时候找的还是fd : 1,但此时,fd : 1 所表示的内容,已经变成了 log.txt 的地址,不再是显示器文件的地址,所以输出的任何消息都会往文件中写入,进而完成输出重定向。
IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是 通过 fd 访问的。
所以C库中 FILE 结构体内部必定封装了 fd,在Linux 和 Windows 中都是这样的。看下图。

Linux下
在这里插入图片描述
Windows下
在这里插入图片描述

4 再谈缓冲区

在这里插入图片描述

由上面的图我们知道,C/C++ FILE 内部有缓冲区 和 文件描述符 fd
0 对应 stdin / cin 1 对应 stdout / cout 2 对应 stderr / cerr

看如下代码:
在这里插入图片描述
结果是

在这里插入图片描述
但如果对进程实现输出重定向呢?./myexe > log, 我们发现结果变成了:

在这里插入图片描述

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和
fork有关!

  • 一般C库函数写入文件是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数是会自带缓冲区,当发生重定向到普通文件的时候,数据的缓冲方式就由行缓冲变成了全缓冲。
  • 而后2个函数输出的数据就在缓冲区中,不会被立刻刷新,直到这个进程结束后,会统一刷新,写入到文件。
  • 但是 fork 的时候,父子数据会发生写时拷贝,所以父子各自私有一份缓冲区内的数据,当父子进程结束就会看到 log中有2份库函数打印的结果。
  • 但是 write 没有变化,说明没有所谓的缓冲。
    总结:printf fwrite 库函数会自带缓冲区,而write 系统调用没有带缓冲区,此外,这里所说的缓冲区都是用户级别的缓冲区,其实为了提升性能,OS 也会提供相关的内核级缓冲区,不过暂不讨论。那么我们一直在唠唠叨叨的缓冲区是谁提供的呢?printf fwrite 是库函数,write 是系统调用,库函数是系统调用的 “上层” ,是对系统调用的"封装",但是 write 没有缓冲区,而 printf fwrite 有,这足以说明该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

5 dup2 系统调用

在这里插入图片描述
dup2 的用法
在这里插入图片描述
**我们将 fd 所在的文件结构体的内容拷贝覆盖到 标准输出显示器所在的文件结构体,则 fd 为 1 已经不再是显示器,而是 我们指定的文件czh.txt **

6 理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据
在这里插入图片描述

每行包括7列

  • 模式
  • 硬链接数
  • 文件所有者
  • 文件所属组
  • 文件大小
  • 文件最后的修改时间
  • 文件名

ls -l读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
在这里插入图片描述

7 动态库和静态库

静态库与动态库

  • 静态库(.a): 程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
  • 动态库(.so): 程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

制作自己的动静态库

可以参考我的GitHub代码,在形成可执行程序后删除静态库也是可以运行的
https://github.com/CZH214926/Linux_C_Cpp

猜你喜欢

转载自blog.csdn.net/CZHLNN/article/details/115111647
今日推荐