当前位置: 首页 > article >正文

Liunx:文件fd、重定向、管道

文件fd: 

        操作系统运行中一定存在着许多被打开的文件,这些文件需要被管理。一个进程会打开若干个文件。一个文件如果在操作系统中被打开,那么必须给该文件创建一个文件对象,包含被打开文件的各种属性。那么进程与文件的关系就变成了PCB和该文件对象的关系。

        未打开的文件被保存在磁盘中,所以fopen注定是一个要访问磁盘的过程,也就是访问IO的过程。

        fopen需要的参数 ,当前路径:指的是进程当前所在的路径。fopen一定是被某个进程所调用的,这个当前路径指的就是该进程的当前路径。进程的属性中是包含了该进程所在的路径的。

fe478a791c4a41399bde212f71a366be.png

        所以调用fopen时,系统会把 进程的当前路径带上,cwd--current work direction。

        如果修改进程的cwd,那么fopen就会把文件默认新建在这个修改后的目录上。chdir该系统调用可以为进程改变当前路径:

dcc64e3115c947c6855165165a790c17.png

        fwrite()在写入之前都会将文件进行清空。cat log > txt .重定向到一个文件每次都是清空的写。效果相当于这里的fwrite()一w的属性打卡文件。

        \0终止符是c的规定,而不是文件的规定。

        C/c++都会默认打开三个输入输出流:

2c68ed7a27974af480e7b38bbd8850a0.png

文件操作系统调用:

        访问文件本质是访问硬件,所有访问硬件的库函数一定要封装系统调用接口。

        2 号man手册。open();

588ae517e3db4c799a72b917c2a5e18b.png

        比特级别的标志位:

5464d61672d9455690594371dc6a456c.png

         open系统调用返回的整形是文件描述符,分配规则是从0下标开始没有被使用的位置。

        0是stdin ,1是stdout,2是stderr。关掉其中之一的输入输出流,然后在使用open打开一个文件,该文件描述符被分配为那个被关掉的io流的描述符,那么从现象上看,close(1),之后再打开文件,之后的cout,printf依旧会向描述符为1的文件中输出,就像是输出被重新定向。所以输出重定向说的就是将进程默认打开的输入输出修改为自己指定的文件。

752f7f6dcd67426ca05a5e895a7836b1.png

 重定向:

        当close关闭某个默认流以后,使用C库函数中的文件操作会出现与open同样的效果。

        即可在用户层或者操作系统层修改文件描述符表,但是用户级别的进程依旧向规定的输出写入。

        修改文件描述符表的系统接口:dup2()

381d76063d1f4471b267ff904262633a.png

        oldfd覆盖newfd

        open系统调用返回的int整形是一个表的下标,而其内容是一个指向文件结构体的指针,将相应文件描述符,即表中相应下标位置的指针修改,就能完成一次重定向,即不再需要手动close()再重新打开;

        这里dup接收的两个整形,注意c的文件接口是一个指针类型的,用c接口打开文件是不知道文件fd,所以dup2 ()通常与open()系统调用配合使用。open()返回的直接是新打开文件的文件描述符。

        当把open()的打开方式的参数设置为追加写时,输出重定向>的效果变成了追加重定向>>。

        read()系统调用的返回值返回一个整形,即读到了多少个字节。read()的参数中有一个指定读多少个字节参数。有时实际读到的大小不一定是期望的大小。这就是这个返回值的含义。

        若将标准输入修改成一个文件,即本来从键盘读,变成从一个文件中读,这就是cat指令的原理。输入重定向。

1f720edfa8e84042bc424847c0ba96c1.png

 进程的替换,包括数据和执行代码的替换与打开的文件之间存在这隔离,进程的替换不会影响当前进程的所打开的关联文件。相应的文件描述符指向哪些文件,进程替换后依旧指向原来的文件。

 为什么要有两个文件描述符指向标准输出

       c0d3ad120d0d48abb5b5e425d6089408.png

        现象是被重定向的只是stdout,保证程序在需要重定向后,依旧可以向显示屏打印错误。

        重定向指令的全写:文件名后加文件描述符

8f6c8fc1d3034d14bb6cbe903f1af757.png

进程间通信 :    

        vscode链接在进程间通信上一节。

        进程间通信:什么是通信,为什么通信,怎么通信?

        通信就是一个进程想把数据给另一个进程,但是因为进程之间具有独立性,所以直接给是不行的,所以必须有通信的方案。

        通信传递数据指令或者进行多进程之间的协同,或者一个进程想通知另一个进程某些事情发生了

        怎么通信?

        首先要让通信的双方看到同一份公共资源,这个资源不能属于通信的任何一方,一般而言,这个资源由操作系统提供,但是操作系统一般不会让进程访问自己的资源,所以要实现进程访问该资源,就要提供一系列的系统调用接口。所以在底层,由操作系统提供通信的方案和一系列接口。一系列的标准被定义。

3af61b6e75d04138b7428e184e5c3dc2.png

e6bad8a04bb047c4937d607122c6fa96.png

        前者使用的最多的,后者需要了解。

        首先,在没有这个标准时,进程间通信可以基于文件的方式进行通信,即管道。

        管道是unix最古老的的通信方式,把从一个进程连接到另一个进程的数据流叫做管道。

        首先一个文件如果可以被多个进程打开并访问,那么该文件不就直接作为公共资源了吗?一个进程写,一个进程读。思想是对的,但是基于文件级的通信,意味着要访问外设,那么随之而来的就是效率问题。

        管道基于上述思想,但是不真正的向磁盘做刷新,可以向内存刷新 。

5c80a41c235e4dc4aa393dadb2c8b9f9.png

        原理、接口与编程:

        a5bce62a87cc4556ac51ef0815ed894d.png        

         操作系统可以为进程打开一种内存级文件,这种文件不真正的在磁盘上有相应的存储,而只是在内存中,并向其他的普通文件一样,有着文件inode,以及一系列方法。

        我们创建一个进程,该进程fork出一个子进程,该子进程复制了父进程的task_struct,包括file_struct,这个file_struct保存着该进程可以访问的文件的详细信息。这张表是被复制的,子进程父进程各有一分,但是该表指向的资源确实同一份:

f0a131b0a6e044fcbff6a5d5bfd78838.png

         这就意味着父子进程可以看到同一份内存级文件。管道就是文件。

        一个问题在于,父进程若是以只读的方式打开文件,那么子进程也只有只读的权限,故两进程都是只读,如何通信??
 

9d940a515ae44957bb6712bfa1744518.png

        解决办法是,父进程以读写的方式打开文件,子进程又具有读写权限,再根据具体场景的需求,父子进程选择性的关闭读写端 。

        一个进程对打开一个文件两次,一次以只读打开,一次以写方式打开。用这种方式是因为,如果对一个文件以读写的方式打开,该进程仅创建一个file结构体,fork()出子进程,相应的复制该结构体,该结构体中仅包含一个文件的偏移量,当你写入以后,偏移量指向文件末尾,那么如何读??子进程每次都要调整文件偏移量再读吗?

        所以我们将一个文件打开两次,OS为该进程创建了两个file结构体,然后父子进程选择性的关闭读写端,那么文件的读写偏移量就互不影响。

        另一个问题是,父子进程各自关闭一个读端一个写端,意味着两个进程之间只能选择单向通信,为什么不将各自进程的读写端都保留,实现双向通信呢??原因在于,虽然文件打开了两次,并且有两个file结构体,但是两个结构体指向的同一个文件缓冲区,当父进程写入,他接下来要进行读,如何判断这次读取的缓冲区的内容是我上次写入的还是子进程写入的??

        如果硬要以这种文件级的形式实现双向通信,当然可以单独的设计出一套数据结构来实现,但是我们直接使用文件这一套现有的结构,目的就是为了简便,所以这里只实现了单向通信。

        所以这也是为什么这种通信方式叫做管道的原因,像生活中的自来水管道等等此类,都是一端入,一端出。

54684b806adf468db9ae567e7086c50f.png

         这种方式的的通信,只要需要通信的进程双方,有一方是直接或者间接被fork()出的,也就是二者之间存在着父子,兄弟或者祖孙关系,就可以实现通信。

        同时这种通信方式,我们不需要内存级文件的名称或者inode,所以叫做再加个定语叫匿名管道。

接口与编程:

        到此为止,上述一直在阐述原理,还没有实现一个真正的通信。下面介绍OS为我们封装的接口。

        man 2 pipe:

        

f3584eb69b6649aab62d474c89435caa.png

        他的作用就是为我们以读写的方式打开一个内存级文件,传入一个输出型参数,将它为我们分别以读和写方式创建的文件描述符返回。0下标为读端,1下标位置为写端。

        

#include<iostream>
#include<unistd.h>
#include<string.h>
#include <sys/types.h>
       #include <sys/wait.h>



using namespace std;

//pipe
//
int main()
{
    pid_t pid_1 =getpid();
    cout<<"main::pid::"<<pid_1<<endl;

    int pipefd[2]={0};
    int n=pipe(pipefd);
    if(n<0)
    {
        cout<<"pipe2() error::"<<endl;
    }
    else
    {
        cout<<"create sucessfully pipe... "<<endl;
    }

    cout<<"fork...."<<endl;
    pid_t ch_pid=fork(); //创建子进程
    if(ch_pid<0)
    {
        cout<<" fork sucessfully ...."<<endl<<endl;
        //sleep(1);
    }

    if(ch_pid==0)
    
    {
        char buffer[1024]="0"; //子进程缓冲区

        //children
        //wirte
        cout<<"child::close side of read..."<<endl;
        sleep(1);
        close(pipefd[0]); //关闭读端

        cout<<"child ::input buffer..."<<endl;

        snprintf(buffer,sizeof(buffer)-1," I am child : pid::%d",getpid());

        //cout<<"child::buffer::content::"<<buffer<<endl;

        sleep(1);
        cout<<"child::write....."<<endl;

        n =write(pipefd[1],buffer,strlen(buffer));
        sleep(1);
        if(n>0)
        {
        cout<<"child::write sucessfull ..."<<endl;
        }
        else 
        {
            cout<<"child::wrte::error..."<<endl;
        }

        //sleep(5);
        

        
    }

    //father
    
    close(pipefd[1]);//关闭写端

    sleep(5); //等待子进程写
    char buffer[1024]="0";
    
    cout<<"father::read...."<<endl;
    n =read(pipefd[0],buffer,sizeof(buffer));
    if(n>0)
    {
        buffer[n]=0;
        cout<<"father::read sucessfully.... "<<endl;;

    }
    cout<<"father::content:"<<buffer<<endl;
    

   n= waitpid(ch_pid,0,0);
   if(n>0)
   {
       cout<<"wait sucessful ..."<<endl;
   }
    
    return 0;
}


http://www.kler.cn/a/385263.html

相关文章:

  • 如何为 Redis 设置密码
  • Linux驱动开发(3):字符设备驱动
  • Vue3中使用LogicFlow实现简单流程图
  • 每天五分钟深度学习框架pytorch:如何加载手写字体数据集mnist?
  • 【数据集】【YOLO】【目标检测】抽烟识别数据集 6953 张,YOLO/VOC格式标注,吸烟检测!
  • 【代码随想录day23】【C++复健】39. 组合总和;40.组合总和II; 131.分割回文串
  • 全局变量之C与Pthon的差异
  • 若依管理系统使用已有 Nacos 部署流程整理
  • [JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
  • 城镇住房保障:SpringBoot系统维护与升级
  • Python基于TensorFlow实现双向循环神经网络GRU加注意力机制分类模型(BiGRU-Attention分类算法)项目实战
  • 多线程案例---阻塞队列
  • RapidrepairController
  • linux 下 signal() 函数的用法,信号类型在哪里定义的?
  • 【go从零单排】go语言中的指针
  • NVR小程序接入平台/设备EasyNVR多品牌NVR管理工具/设备汇聚公共资源场景方案全析
  • 如何设置 TORCH_CUDA_ARCH_LIST 环境变量以优化 PyTorch 性能
  • AutoOps 使每个 Elasticsearch 部署都更易于管理
  • C#核心(7)索引器
  • 从0开始linux(21)——文件(2)文件重定向
  • Hive 查询各类型专利 Top 10 申请人及对应的专利申请数
  • 记录offcanvas不能显示和关闭的修复方法
  • QT监控文件夹变化(文件增加、删除、改名)
  • B2C分销管理系统(源码+文档+部署+讲解)
  • C++20 STL CookBook 4:使用range在容器中创建view
  • c# 动态lambda实现二级过滤(多种参数类型)