【Linux系统编程】第二十七弹---文件描述符与重定向:fd奥秘、dup2应用与Shell重定向实战

news/2024/10/4 17:09:35/

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、文件描述符fd

1.1、0 & 1 & 2 

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

2、重定向

3、使用 dup2 系统调用

3.1、> 输出重定向

3.2、>> 追加重定向

3.3、< 输入重定向

3.4、shell模拟实现> >> <

4、缓冲区 


1、文件描述符fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

查看open的返回值fd。

会用到的头文件

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

代码演示 

int main()
{// 查看open返回值是什么int fda = open("loga.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);printf("fda : %d\n",fda);int fdb = open("logb.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);printf("fdb : %d\n",fdb);int fdc = open("logc.txt",O_WRONLY | O_CREAT | O_TRUNC,066);printf("fdc : %d\n",fdc);int fdd = open("logd.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);printf("fdd : %d\n",fdd);return 0;
}

运行结果 

结果是从3开始,且是依次递增的。

1.1、0 & 1 & 2 

fd为什么从3开始呢?0 1 2分别代表什么呢?

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2,根据文件描述符分配规则(后序一个标题详细讲解),找到当前没有被使用的最小的一个下标,作为新的文件描述符
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器。

补充(使用系统调用读文件):

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);

尝试从文件描述符 fd 读取最多 count 个字节到从 buf 开始的缓冲区。

验证一

int main()
{char buf[1024];// 从键盘读取sizeof(buf)个字节到buf中ssize_t s = read(0, buf, sizeof(buf));if(s > 0){buf[s] = 0;// 设置结尾\0// 将buf的strlen(buf)长度写到显示器中write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
}

运行结果 

内核结构 

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

进一步验证

fd在C语言层面其实是FILE结构体中的一个 _fileno 成员,我们可以打印这个成员的结果来验证0,1,2。

代码演示  

int main()
{//0 1 2 默认打开 printf("stdin->fd: %d\n",stdin->_fileno);printf("stdout->fd: %d\n",stdout->_fileno);printf("stderr->fd: %d\n",stderr->_fileno);// 普通文件创建的FILE* fp = fopen("log.txt","w");if(fp == NULL) return 1;printf("fd: %d\n",fp->_fileno);FILE* fp1 = fopen("log1.txt","w");if(fp == NULL) return 1;printf("fd: %d\n",fp1->_fileno);FILE* fp2 = fopen("log2.txt","w");if(fp == NULL) return 1;printf("fd: %d\n",fp2->_fileno);return 0;
}

运行结果 

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

关闭0或者2

int main()
{close(2);//close(0);int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd < 0) {perror("open");return 1;}printf("fd: %d\n",fd);return 0;
}

运行结果 

从关闭0号和2号文件描述符我们可以看到,关闭几号创建新文件的fd就是几号。 

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

2、重定向

代码演示 

int main()
{close(1);int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd < 0){perror("open");return 1;}printf("fd: %d\n",fd);fflush(stdout);close(fd);return 0;
}

运行结果 

此时,我们发现,本来应该输出到显示器上的内容输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <。


那重定向的本质是什么呢? 

上层用的 fd 不变,在内核中更改 fd 对应的 struct file* 地址。

3、使用 dup2 系统调用
 

#include <unistd.h>int dup2(int oldfd, int newfd);

文件描述符下标内容的拷贝,将oldfd拷贝给newfd。

3.1、> 输出重定向

将新文件描述符下标内容拷贝到显示器上。

代码演示  

int main()
{int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd < 0){perror("open");return 1;}dup2(fd,1);printf("hello linux\n");return 0;
}

运行结果 

3.2、>> 追加重定向

将新文件描述符下标内容追加拷贝到显示器上。

代码演示  

int main()
{int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);if(fd < 0){perror("open");return 1;}dup2(fd,1);printf("hello linux\n");fprintf(stdout,"hello linux\n"); return 0;
}

运行结果 

3.3、< 输入重定向

 将新文件描述符下标内容拷贝到键盘上。

代码演示  

int main()
{int fd=open("log.txt",O_RDONLY);if(fd == -1){perror("open");return 1;}//输入重定向dup2(fd,0);char outbuffer[64];while(1){// 获取fd = 0中数据,即文件if(fgets(outbuffer,sizeof(outbuffer),stdin) == NULL) break;printf("<%s",outbuffer);}return 0;
}

运行结果 

3.4、shell模拟实现> >> <

头文件、宏、全局变量

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
#define SkipSpace(cmd, pos) do{\while(1){\if(isspace(cmd[pos]))\pos++;\else break;\}\
}while(0)char* gArgv[NUM];
int lastcode = 0;
char cwd[SIZE*2];// "ls -a -l -n > myfile.txt"
#define None_Redir 0
#define In_Redir   1
#define Out_Redir  2
#define App_Redir  3int redir_type = None_Redir;
char *filename = NULL;

1.CheckRedir

在获取字符串之后检查是否有重定向符号,通过全局变量redir_type赋予不同的值,默认没有重定向符号。

代码演示  

void CheckRedir(char cmd[])
{// > >> <// "ls -a -l > myfile.txt"int pos = 0;int end = strlen(cmd);while(pos < end){if(cmd[pos] == '>'){if(cmd[pos + 1] == '>'){cmd[pos++] = 0;pos++;redir_type = App_Redir;SkipSpace(cmd,pos);filename = cmd + pos;}else {cmd[pos++] = 0;redir_type = Out_Redir;SkipSpace(cmd,pos);filename = cmd + pos;}}else if(cmd[pos] == '<'){cmd[pos++] = 0;redir_type = In_Redir;SkipSpace(cmd,pos);filename = cmd + pos;}else {pos++;}}
}

2.测试CheckRedir

 打印出对应的变量值即可。

    printf("cmd: %s\n",usercommand);printf("redir: %d\n",redir_type);printf("filename: %s\n",filename);

运行结果 

4、缓冲区 

缓冲区是什么?

缓冲区是一段内存空间。

为什么要有缓冲区?

给上层提供高效的IO体验,间接提高整体的效率。

缓冲区的刷新策略

正常情况

        1、立即刷新。fflush(stdout)  int fsync(int fd); synchronize a file's in-core state with storage device。

        2、行刷新。显示器刷新,为了照顾用户的体验。

        3、全缓冲。缓冲区写满才刷新(普通文件)。

特殊情况

        1、进程退出,系统自动刷新

        2、强制刷新

缓冲器包括?

用户级缓冲区 内核级缓冲区

缓冲区的意义:

1、解耦

2、提高效率,提高用户使用的效率,提高刷新IO的效率

代码演示  

int main()
{printf("hello printf\n");fprintf(stdout,"hello fprintf\n");const char* msg = "hello write\n";write(1,msg,strlen(msg));return 0;
}

运行结果 

奇怪的代码 

int main()
{printf("hello printf\n");fprintf(stdout,"hello fprintf\n");const char* msg = "hello write\n";write(1,msg,strlen(msg));fork();return 0;
}

运行结果 

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

  •  一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲

综上printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:


typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

在/usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

http://www.ppmy.cn/news/1534524.html

相关文章

Ubuntu2404安装

Ubuntu是一款非常优秀的发行版本&#xff0c;起初她的优势主要在于桌面版&#xff0c;但是随着Centos 从服务版的支持的退出&#xff0c;Ubuntu server也在迅猛的成长&#xff0c;并且不断收获了用户&#xff0c;拥有了一大批忠实的粉丝。好了&#xff0c;废话不多说&#xff0…

C++学习笔记----8、掌握类与对象(二)---- 成员函数的更多知识(3)

3.3、引用验证的成员函数 通常的成员函数可以被非临时和临时的类实例调用。假设你有如下的类只是记住了string作为参数传递给了构造函数&#xff1a; class TextHolder { public:explicit TextHolder(string text) : m_text { move(text) } {}const string& getText() con…

Android 开启相机一键拍照,一键录制

一键拍照&#xff1a; Intent intent new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { context.startActivity(intent); …

Edge SCDN:安全与速度并进的解决方案

在当今数字化时代&#xff0c;网络速度与安全成为企业不可忽视的双刃剑。Edge SCDN&#xff08;边缘安全加速&#xff09;正重新定义着内容分发与安全防护的边界&#xff0c;它不仅优化了内容传输的速度&#xff0c;还强化了网络的安全防护能力&#xff0c;为企业构建了一个既快…

Django Web开发接口定义

Django Web 介绍 Django Web是一个Pyhton高级 Web 框架,实际上 Django 也可以做到前后端分离,即主要作为后端框架使用,不用模板渲染也是可行的。 Django Web 应用的运行流程,如下图所示: 此外,Django Web 在开发环境可以通过自带的服务器进行本地调试。但是该服务器不适…

【Android】【bug】ImageView设置scaleType不生效的问题

问题 在设置ImageView的时候&#xff0c;添加了一张图片&#xff0c;图片是正方形的&#xff0c;但是图片要尽可能的占据控件&#xff0c;同时也要保证图片是不能变形的。 然后在图片里面使用了这个scaleType <ImageViewandroid:id"id/iv_luopan"android:layout…

基于SpringBoot+Vue+MySQL的旅游网站

系统展示 用户前台界面 管理员后台界面 系统背景 随着社会的不断发展和人们生活水平的提高&#xff0c;旅游活动逐渐成为人们生活中不可或缺的一部分。传统的旅游服务方式存在信息不对称、服务流程繁琐等问题。为了改善用户体验、提高服务效率&#xff0c;采用现代化的技术手段…

Flask+微信小程序实现Login+Profile

Python代码 首先flask的session用不了&#xff0c;只能用全局变量来实现。 import pymysql from flask import Flask, request, jsonify, session from flask_cors import CORS from flask import make_responseapp Flask(__name__) CORS(app, supports_credentialsTrue) #…