重定向和文件描述符(file descriptors)

  一、标准错误重定向

    命令可以将输出发送到 stderr 。默认情况下,shell将stderr导向屏幕。因此,除非我们重定向,否则我们可能不知道命令发送到stdout的输出和发送到stderr的输出之间的区别。

   在读取或写入文件之前,必须先打开该文件。内核为每个进程维护一个打开的文件列表。该列表称为文件表。该表通过称为文件描述符的非负整数进行索引。列表中的每个条目都包含有关打开文件的信息。打开一个 f 会返回一个文件描述符,read/write 会将该文件描述符作为参数。

   文件描述符(file descriptors )是程序将输出发送到和从中获取输入的地方。文件描述符与用户空间共享,并由用户程序直接用于访问文件。

 二、文件描述符(file descriptors )
      当我们执行程序时,Linux会为所有的为程序都分配三个文件描述符
      但是这个三个文件描述符都会指向你的终端的pts,这个要注意:前提是tty以pty模式作为tty驱动(详细参看 这里)

      0  、标准输入(stdin)  ,默认是从键盘输入的任何文件,它的流id=0
      1  、标准输出 (stdout),默认显示在屏幕上,流id=1
      2 、标准错误 (stderr), 是命令产生的错误消息(如果有的话)。默认情况下,stderr 也会显示在屏幕上。它的流id=2
      255、表示始终连接到tty上

      当某个进程成功请求打开文件时,内核返回一个文件描述符,该文件描述符指向内核全局文件表中的一个条目。文件table entry(表条目)包含文件的inode(索引节点)、字节偏移量以及该数据流的访问限制(只读、只写等)等信息。下图指0 1 2 之后的文件描述符

文件描述符图



         

 

      当你交互式shell 远程登录之后,本环境是bash(gnu),bash读取初始化的配置文件之后,并且通过agtty得到tty,同时分配了master,slave(pts编号为6)

[root@aozhejin2 /dev]$ll stderr stdout stdin
lrwxrwxrwx 1 root root 15 Apr 21  2022 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Apr 21  2022 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Apr 21  2022 stdout -> /proc/self/fd/1
//交互式shell登录以后/dev/下的stderr、stdout、stdin等都指向当前的进程的fd,说白了就是当前pts
我有两个终端登录.
第一个pts/6, /proc/self指向了 /proc/self/cwd-->指向某个进程比如/proc/12 他们下的/proc/52563/fd内放置了具体的0 1 2 3...
第二个pts/7,
/proc/self指向了 /proc/self/cws-->指向某个进程比如/proc/13 他们下的/proc/74443/fd内放置了具体的0 1 2 3 4...
[root@aozhejin2 /proc/self]$ll
lrwxrwxrwx 1 root root 0 Mar 27 18:22 cwd -> /proc/74443 ,这个才是真正的负责交互的进程,本终端pts/7

 做个小实验   

#include <fcntl.h> 
#include <stdio.h> 
#include <errno.h>
int main(void)
{
 int fd=open("/usr/local/src/stracetest/aozhejin", O_CREAT|O_RDONLY);
 if(fd<0){
    printf("errno is %d\n",errno);

 }
 /* 打开,如果不存在则创建文件/usr/local/src/stracetest/aozhejin */
 sleep(20000);
 return 0;
 //https://man7.org/linux/man-pages/man2/open.2.html
}
编译gcc teststrace.c
启动 ./a.out

 然后我启动之后,只要创建进程默认一定会在/proc/pid/fd 下创建 0 1 2 这三个文件描述符,他们作用不同,都指向你登录终端的pts设备,因为本进程还会创建一个文件,所以会分配其它文件描述符,我们在另一个终端查看相关情况

  [root@aozhejin2 /dev]$ps -ef| grep a.out
  root 39298  17320 0 17:16 pts/6 00:00:00 ./a.out

[root@aozhejin2 /proc/39298/fd]$ls -l  
total 0
lrwx------ 1 root root 64 Mar 28 15:11 0 -> /dev/pts/7  #0表示输入,
lrwx------ 1 root root 64 Mar 28 15:11 1 -> /dev/pts/7  #1表示输出
lrwx------ 1 root root 64 Mar 28 15:11 2 -> /dev/pts/7  #2表示错误
lr-x------ 1 root root 64 Mar 28 15:11 3 -> /usr/local/src/stracetest/aozhejin  #文件描述符
lrwx------ 1 root root 64 Mar 28 15:11 6 -> /dev/pts/7
我们查看inode

[root@aozhejin2 /proc/47323/fd]$stat 3 

File: ‘3’ -> ‘/usr/local/src/stracetest/aozhejin’
Size: 64 Blocks: 0 IO Block: 1024 symbolic link
Device: 3h/3d Inode: 638075231 Links: 1
Access: (0500/lr-x------) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-03-28 17:16:45.366291259 +0800
Modify: 2023-03-28 17:16:45.365291259 +0800
Change: 2023-03-28 17:16:45.365291259 +0800
Birth: -
  这里注意我直接stat了文件描述符,然后我用readlink找出符号链接所指向的位置

[root@aozhejin2 /proc/39298/fd]$readlink 3
/usr/local/src/stracetest/aozhejin

  

   三、关于/dev/null(设备号1,3即主,次)
        /dev/null。它是存在于每个 Linux 系统中的特殊文件。但是,与大多数其他虚拟文件不同,它不是用于读取,而是用于写入。无论你写入 /dev/null 什么都会被丢弃,被遗忘到虚无中。它在 UNIX 系统中被称为空设备(linux官方解释为null dev)。
       /dev/null 是最广为人知的 /dev/ 设备文件。显然,这是一个名为“null”的文件,位于“/dev/”目录中。“/dev/”目录是 devfs(设备文件系统)伪文件系统的挂载点。devfs 是一个文件系统形式的设备管理器,它将每个设备显示为文件。这个伪/虚拟文件系统不在硬盘上。相反,它在记忆中。“文件”由内核生成,以提供软件和进程(包括用户态程序和脚本)对设备的访问

[root@aozhejin2 /dev/null]$ll | grep null   #特殊设备
 crw-rw-rw- 1 root root 1, 3 Apr 21 2022 null   //1,3主设备号1,次设备号3
你如果 cat /proc/devices ,并没有列出来,
但是你可以参看 https://www.kernel.org/doc/Documentation/admin-guide/devices.txt 标注的是
Null device

         我们看下fake file

[root@aozhejin2 /dev]$stat /dev/null
  File: ‘/dev/null’
  Size: 0             Blocks: 0          IO Block: 4096   character special file
Device: 5h/5d    Inode: 1028        Links: 1     Device type: 1,3
Access: (0666/crw-rw-rw-)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-04-21 08:57:42.086999692 +0800   
Modify: 2022-04-21 08:57:42.086999692 +0800
Change: 2022-04-21 08:57:42.086999692 +0800
 Birth: -
1.crw标识是一个字符设备,主设备号是1,次设备号3
#2.创建时间和我启动时间一致

  3.UID和GID都是0,标识每个用户都可以写入/dev/null
  4.你填不满这个设备

[root@aozhejin2 /dev]$file /dev/null
/dev/null: character special

[root@aozhejin2 /dev]$ll /dev/null
crw-rw-rw- 1 root root 1, 3 Apr 21 2022 /dev/null

[root@aozhejin2 /dev]$date | /dev/null
-bash: /dev/null: Permission denied

我们收到错误“-bash: /dev/null: Permission denied” ,原因是:  /dev/null 不是可以接受管道的可执行文件

#/dev/null,主设备号来自 1 (mem) ,我们看驱动源码也能只是/dev/zero等都在mem里面

[root@aozhejin2 /usr/local/src]$cat /proc/devices
Character devices:
1 mem

 

     /dev/null设备号

https://www.kernel.org/doc/Documentation/admin-guide/devices.txt  ,主备号都是1即mem ,次设备号,它和其它驱动不一样
1 char Memory devices 1 = /dev/mem Physical memory access 2 = /dev/kmem OBSOLETE - replaced by /proc/kcore 3 = /dev/null Null device (1,3) 4 = /dev/port I/O port access 5 = /dev/zero Null byte source 6 = /dev/core OBSOLETE - replaced by /proc/kcore 7 = /dev/full Returns ENOSPC on write 8 = /dev/random Nondeterministic random number gen. 9 = /dev/urandom Faster, less secure random number gen. 10 = /dev/aio Asynchronous I/O notification interface 11 = /dev/kmsg Writes to this come out as printk's, reads export the buffered printk records. 12 = /dev/oldmem OBSOLETE - replaced by /proc/vmcore


    要正确地将数据发送到 /dev/null,请使用诸如“>”之类的重定向。例如,“date > /dev/null”会将“date”命令的输出写入/dev/null,就好像它是一个文件一样。不会看到任何输出;该数据已永久消失。写入 /dev/null 的任何数据都不可恢复。使用 /dev/null 时,应用程序感知到 I/O 操作,尽管硬件上没有发生 I/O 操作。当数据被发送到 /dev/null 时,数据流很快就会从存在中删除。例如,运行命令“time (dmesg)”比“time (dmesg > /dev/null)”花费更多的时间来执行。换句话说,写入 /dev/null 比打印到屏幕或将数据发送到其他地方更快。
      /dev/null 可用于删除错误消息。例如,如果不希望在输出中得到错误消息,则将错误重定向到 /dev/null。语法是“CMD 2> /dev/null”(用命令替换“CMD”)。此外,要将输出(但不是错误)保存到文件中,用户可以使用类似“CMD 2> /dev/null > /PATH/FILE.TXT”的语法。但是,要将良好数据 (stdout) 保存到文件,但允许错误 (stderr) 显示在屏幕上,请使用类似“CMD 1> /PATH/FILE.TXT”的格式。 

注意:写入 /dev/null 时,将其视为黑洞。任何进入它的东西都消失了,永远不会回来。

我们利用strace来分析下

[root@aozhejin2 /usr/local/src]$strace cat /dev/null 
execve("/usr/bin/cat", ["cat", "/dev/null"], 0x7ffcb8ca14a8 /* 32 vars */) = 0
brk(NULL)                               = 0xdae000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4559666000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=47788, ...}) = 0
mmap(NULL, 47788, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f455965a000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2156592, ...}) = 0
mmap(NULL, 3985920, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4559078000
mprotect(0x7f455923c000, 2093056, PROT_NONE) = 0
mmap(0x7f455943b000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) = 0x7f455943b000
mmap(0x7f4559441000, 16896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4559441000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4559659000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4559657000
arch_prctl(ARCH_SET_FS, 0x7f4559657740) = 0
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
mprotect(0x7f455943b000, 16384, PROT_READ) = 0
mprotect(0x60b000, 4096, PROT_READ)     = 0
mprotect(0x7f4559667000, 4096, PROT_READ) = 0
munmap(0x7f455965a000, 47788)           = 0
brk(NULL)                               = 0xdae000
brk(0xdcf000)                           = 0xdcf000
brk(NULL)                               = 0xdcf000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=106176928, ...}) = 0
mmap(NULL, 106176928, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4552b35000
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 6), ...}) = 0
open("/dev/null", O_RDONLY)             = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
read(3, "", 65536)                      = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++
https://man7.org/linux/man-pages/man1/strace.1.html
我们只关注 跟踪open函数的调用.

[root@aozhejin2 /usr/local/src]$strace -e open cat /dev/null
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
open("/dev/null", O_RDONLY) = 3
+++ exited with 0 +++

  

它说它读取了与关联的文件描述符的内容/dev/null,并返回了 0。man read 解释:“成功时,返回读取的字节数(0表示文件结尾)”。所以,它什么都不返回。很明显,本应为“null”的内容绝对不会返回任何内容。 (参考 man fstat)

我们用strace 来继续测试一下 /dev/null

[root@aozhejin2 /usr/local/src]$strace -c ls > /dev/null 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 18.89    0.000129           7        18           mprotect
 12.15    0.000083           2        28           mmap
  9.66    0.000066           4        14           close
  6.59    0.000045           4        11           open
  6.59    0.000045          15         3           munmap
  5.71    0.000039           3        10           read
  5.42    0.000037           3        12           fstat
  5.12    0.000035           8         4         3 access
  4.10    0.000028          14         2         2 statfs
  3.95    0.000027           9         3         3 ioctl
  3.95    0.000027          13         2           getdents
  2.78    0.000019           6         3           brk
  2.78    0.000019          19         1           openat
  2.49    0.000017           8         2           rt_sigaction
  2.20    0.000015          15         1         1 stat
  1.32    0.000009           9         1           write
  1.32    0.000009           9         1           rt_sigprocmask
  1.32    0.000009           9         1           getrlimit
  1.32    0.000009           9         1           set_tid_address
  1.17    0.000008           8         1           arch_prctl
  1.17    0.000008           8         1           set_robust_list
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ----------------
100.00    0.000683                   121         9 total

 


/dev/null 源代码

         
      /dev/null 和 /dev/zero 设备文件的源代码位于内核源码 “E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\drivers\char\mem.c”下(https://github.com/torvalds/linux/blob/master/drivers/char/mem.c). 此源代码文件包含许多驻留在 devfs 上的字符设备的代码

...
static
const struct memdev { const char *name; mode_t mode; const struct file_operations *fops; struct backing_dev_info *dev_info; } devlist[] = { [1] = { "mem", 0, &mem_fops, &directly_mappable_cdev_bdi }, #ifdef CONFIG_DEVKMEM [2] = { "kmem", 0, &kmem_fops, &directly_mappable_cdev_bdi }, #endif [3] = { "null", 0666, &null_fops, NULL }, #ifdef CONFIG_DEVPORT [4] = { "port", 0, &port_fops, NULL }, #endif [5] = { "zero", 0666, &zero_fops, &zero_bdi }, [7] = { "full", 0666, &full_fops, NULL }, [8] = { "random", 0666, &random_fops, NULL }, [9] = { "urandom", 0666, &urandom_fops, NULL }, [11] = { "kmsg", 0, &kmsg_fops, NULL }, #ifdef CONFIG_CRASH_DUMP [12] = { "oldmem", 0, &oldmem_fops, NULL }, #endif };
...



    四、  重定向表

 

重定向类型 重定向标记符 重定向标记符缩写 解释和说明
stdout重定向  1>  > 把输出重定向到某处,缩写是我们经常用的
如果您重定向到一个已经存在的文件,shell 将首先擦除该文件。
这意味着输出文件的现有内容将被删除并替换为命令的输出。
    >> 把输出重定向到某处,追加模式,不会覆盖
stderr重定向  2>    错误重定向
   2>>   追加重定向,错误信息不会覆盖
stdin重定向 0<   标准输入重定向
     | 管道重定向即将一个命令的标准输出发送到另一个命令的标准输入 
输出和错误重定向  &>   意义相同  &>file  不会有任何显示,包括stdout和stderr都会放入到file里面
  2&1  stderr 和 stdout的组合,表示声明文件描述符2 是文件描述符1的副本
    /dev/null 扔到空设备里

         解释:
    1、重定向输出符号(>)是 1> 的缩写,它告诉shell重定向标准输出。
         2、 < 是 0< 的缩写,它重定向标准输入。
         3、 2>  重定向标准错误。


   五、实例讲解
       
    1、输出重定向

       在下面的示例中,我们将看到如何将标准输出和标准错误重定向到不同的文件和同一个文件。当我们使用不存在的文件名和存在的文件名称运行cat时,cat会向标准错误发送错误消息,并将存在的文件复制到标准输出。除非我们重定向它们,否则这两条消息都会出现在屏幕上。

[root@aozhejin2 /usr/local/src]$ cat savefile
123
[root@aozhejin2 /usr/local/src]$ cat errfile
cat: errfile: No such file or directory

[root@aozhejin2 /usr/local/src]$ cat savefile errfile
123
cat: errfile: No such file or directory

   当我们重定向stdout时,发送到stderr的输出不受影响,仍然显示在屏幕上:

[root@aozhejin2 /usr/local/src]$ cat file1 file2 > out   //如果out不存在,shell会创建它
cat: file2: No such file or directory
[root@aozhejin2 /usr/local/src]$ cat out
123
 > 是 重定向输出 的缩写
$ echo "aozhejin" > out  #这个覆盖上面的文件内容

 
2、管道
  类似地,当我们通过管道发送标准输出时,stderr错误不会受到影响。下面的示例通过管道将cat的stdout发送到tr,tr将小写转换为大写。cat发送给stderr的文本没有被翻译,因为它直接进入屏幕,而不是通过管道。

[root@aozhejin2 /usr/local/src]$ cat savefile errfile | tr "[a-z]" "[A-Z]"
123
cat: errfile: No such file or directory

 
3、组合输出
下一个示例将标准输出和标准错误重定向到不同的文件。2> 告诉shell将标准错误重定向到何处,即文件描述符2 。

告诉shell将标准输出重定向到何处,即文件描述符 1。我们可以用 > 来代替 1>。

[root@aozhejin2 /usr/local/src]$ $ cat savefile  errfile  1>std_out 2> err_out   #不会有任何输出
等价于
[root@aozhejin2 /usr/local/src]$ cat savefile errfile > std_out 2> err_out

  [root@aozhejin2 /usr/local/src]$cat std_out
   123

  [root@aozhejin2 /usr/local/src]$cat err_out
   cat: errfile: No such file or directory

4、&> 重定向

  在以下示例中,&> 将 stdout和 stderr一块重定向到单个文件(错误不会显示)

//下面的不会有任何输出,错误不会显示,savefile里面内容为123
[root@aozhejin2 /usr/local/src]$cat savefile errfile &> std_err_out

//错误和输出都放到里面了
[root@aozhejin2 /usr/local/src]$ cat std_err_out
123
cat: errfile: No such file or directory

 

 5、2&1 重定向输出

 在下面的示例中,第一个 1> 将stdout重定向输出到std_out, 2>&1 声明文件描述符2 是文件描述符1的副本
 因此,标准输出和标准错误都被重定向到std_out。(效果就是把 > 的标准输入输出出来)

[root@aozhejin2 /usr/local/src]$ cat savefile errfile 1> std_out 2>&1  #执行此命令不会有任何结果输出
//查看std_out,savefile内容写入std_out,并且错误信息也被写入里面
[root@aozhejin2
/usr/local/src]$cat std_out 123 cat: errfile: No such file or directory

 

在2>&1中,我们可能希望保留重定向的顺序,否则我们可能无法获得预期的结果。


以下示例将文件描述符声明为文件描述符1的副本,并通过管道将文件描述符1输出发送到tr命令。(组合使用重定向)


[root@aozhejin2 /usr/local/src]$cat file1 file2 2>&1 | tr "[a-z]" "[A-Z]"
ABC
CAT: FILE2: NO SUCH FILE OR DIRECTORY


追加文件中利用>>
[root@aozhejin2 /usr/local/src]$echo "aozhejin" | cat >>file1
[root@aozhejin2 /usr/local/src]$cat file1
abc
aozhejin


6、/dev/null

   如果我们不想保存输出,或你忘记了重定向输入错误、输出以及输入,我们可以将其发送到/dev/null,这个比较容易记忆:

[root@aozhejin2 /usr/local/src]$pirntf "%s\n" "Hello" 2> /dev/null
把错误直接扔到空设备里面了,不会有任何输出(所有错误扔到null设备里面去)

[root@aozhejin2 /usr/local/src]$pirntf "%s\n" "aozhejin" 2> /dev/null 2>&1  #经过重定向之后输出错误
-bash: pirntf: command not found

2> 重定向输出错误

 我们只想关注输出之后的错误怎么处理了? 那么就用 1> /dev/null 吧,因为错误信息会由标准错误来处理,这里我们相当于只处理了标准输出。所以这个会把错误输出出来,正常的内容扔到/dev/null里面

 比如: 有一个比较有用的例子,如果你想ping 一个ip,只想把错误输出出来,也可以使用/dev/null
 stdout的内容被转储到 /dev/null,只留下错误。

ping www.aasdfsdfwds.org 1> /dev/null
等价于
ping www.aasdfsdfwds.org > /dev/null

   我们测试使用了 2> 重定向错误、/dev/null 空设备、> 重定向输入结果如何,我测试一下。

[root@aozhejin2 /usr/local/src]$pirntf "%s\n" "aozhejin" 2> /dev/null > errorfile.txt
[root@aozhejin2 /usr/local/src]$cat errorfile.txt 
//什么也没有,因为2> 重定向到了空设备,再次重定向 > 也就没有意义了

  以下什么也不会输出

[root@aozhejin2 /usr/local/src]$ls > /dev/null 2>&1
[root@aozhejin2 /usr/local/src]$ls a > /dev/null 2>&1

 

https://www.bogotobogo.com/Linux/linux_tips2_bash.php
https://docs.kernel.org/filesystems/caching/cachefiles.html
https://tldp.org/LDP/sag/html/dev-fs.html
https://www.kernel.org/doc/Documentation/admin-guide/devices.txt
https://linuxhint.com/what_is_dev_null/
https://linuxhint.com/bash_stdin_stderr_stdout/
https://www.linux.org/threads/overview-of-dev-null.11641/
https://linuxhandbook.com/redirection-linux/
https://linux.die.net/man/2/fstat
  bash的重定向
https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Redirections  

posted @ 2023-03-27 19:24  jinzi  阅读(323)  评论(0编辑  收藏  举报