天天看點

ls proc $$ self fd 3 255 引發的一些瑣事

我在使用bash的時候通常會利用它的自動補全功能來看看檔案夾下的内容(連按兩下

Tab

鍵),例如:
ls proc $$ self fd 3 255 引發的一些瑣事
說明Music檔案夾下有這三個檔案,我也就不需要提前用

ls

指令來确定了。

但是最近我在檢視目前shell(bash)的檔案描述符時時卻碰見一個“怪事”,當我用bash的自動補全功能檢視時,顯示為有0, 1, 2, 255, 3這五個檔案:

ls proc $$ self fd 3 255 引發的一些瑣事
但是當我用

ls

指令來顯示fd檔案夾的時候,卻隻顯示有0, 1, 2, 255這4個檔案,3這個檔案不存在:
ls proc $$ self fd 3 255 引發的一些瑣事

這是為什麼呢?

其實原因很簡單,自動補全功能是bash内置的一個功能,而

ls

是系統上的一個程式,以子程序的形式獨立于bash運作。是以如果bash這個自動補全功能打開了我們要補全的路徑(檔案夾也是檔案),那麼應該會獲得檔案描述符3,

ls

也是一樣。但是5736這個PID是bash的,是以我們用ls的時候看不到3而用bash的自動補全功能看得到。

為了證明一下這個的想法,上網查了一下相關資料,了解到bash自動補全功能本身就是一個用shell語言寫的腳本,其配置在

/etc/bash_completion

這個檔案中,其中常用的内置指令是

complete

,用法為

complete -F _known_hosts xvncviewer

,即當開頭的指令./程式是

xvncviewer

的時候,如果使用者在參數上連按

Tab

鍵就會調用

_known_hosts

這個shell内置函數 ,例如:

skx@lappy:~$ xvncviewer s[TAB]
savannah.gnu.org            ssh.tardis.ed.ac.uk
scratchy                    steve.org.uk
security.debian.org         security-master.debian.org
sun
skx@lappy:~$ xvncviewer sc[TAB]
           

我們進入

/etc/bash_completion

檔案,查找剛剛使用的

ls

指令,看看它的自動補全是什麼配置的:

complete -F _longopt a2ps awk base64 bash bc bison cat colordiff cp csplit \
    cut date df diff dir du enscript env expand fmt fold gperf \
    grep grub head indent irb ld ldd less ln ls m4 md5sum mkdir mkfifo mknod \
    mv netstat nl nm objcopy objdump od paste pr ptx readelf rm rmdir \
    sed seq sha{,1,224,256,384,512}sum shar sort split strip sum tac tail tee \
    texindex touch tr uname unexpand uniq units vdir wc who
           

可以看到,其調用的是

_longopt

這個内置函數,繼續定位:

_longopt()
{
    local cur prev words cword split
    _init_completion -s || return

    case "${prev,,}" in
        --help|--usage|--version)
            return 0
            ;;
        --*dir*)
            _filedir -d
            return 0
            ;;
        --*file*|--*path*)
            _filedir
            return 0
            ;;
        --+([-a-z0-9_]))
            local argtype=$( $1 --help 2>&1 | sed -ne \
                "s|.*$prev\[\{0,1\}=[<[]\{0,1\}\([-A-Za-z0-9_]\{1,\}\).*|\1|p" )
            case ${argtype,,} in
                *dir*)
                    _filedir -d
                    return 0
                    ;;
#......省略
           

可以看到

_longopt

會調用

_filedir

這個函數:

_filedir()
{
    local i IFS=$'\n' xspec

    _tilde "$cur" || return 0

    local -a toks
    local quoted x tmp

    _quote_readline_by_ref "$cur" quoted
    x=$( compgen -d -- "$quoted" ) &&
    while read -r tmp; do
        toks+=( "$tmp" )
    done <<< "$x"

    if [[ "$1" != -d ]]; then
        # Munge xspec to contain uppercase version too
        # http://thread.gmane.org/gmane.comp.shells.bash.bugs/15294/focus=15306
        xspec=${1:+"!*.@($1|${1^^})"}
        x=$( compgen -f -X "$xspec" -- $quoted ) &&
        while read -r tmp; do
            toks+=( "$tmp" )
        done <<< "$x"
    fi

    # If the filter failed to produce anything, try without it if configured to
    [[ -n ${COMP_FILEDIR_FALLBACK:-} && \
        -n "$1" && "$1" != -d && ${#toks[@]} -lt 1 ]] && \
        x=$( compgen -f -- $quoted ) &&
#......省略
           

可以看到該函數使用了

compgen

這個内置指令來擷取檔案夾下的檔案名(-f = "filename"),例如:

ls proc $$ self fd 3 255 引發的一些瑣事

我們使用

strace

來追蹤這個内置指令的系統調用,特别是傳回檔案描述符的系統調用

open

ls proc $$ self fd 3 255 引發的一些瑣事

通過對比可以看到

compgen

調用

open

打開了這個檔案夾,而且得到了檔案描述符3(前面的open都調用了

close

删除了它們得到的檔案描述符3)。

如果将

compgen

換成

ls

ls proc $$ self fd 3 255 引發的一些瑣事

對比可以看出,

compgen

隻有一個

execve

,即

compgen

是在bash程序中執行的,但

ls

有兩個,第二個說明了它是作為bash的子程序運作的, 證明了我們之前的想法。

如果感興趣的話可以看看

ls

的源碼,其中使用到了

readdir

opendir

這兩個庫函數(GNU coreutils-8.29)

綜上,我們可以用兩個圖來總結。

自動補全:

Process: Bash
+-----------------------------+
|                             |
|  0,1,2,255       0,1,2,3,255|
|    Tab->compgen->open       |
|                             |
+-----------------------------+
           

ls

指令:

Process: Bash
+-----------------------------+
|                             |
|  0,1,2,255                  |
|     ls                      |
|      +                      |
+-----------------------------+
       |
       |
       | Child Process: ls
+------+----------------------+
|                             |
|0,1,2         0,1,2,3        |
|  opendir->open              |
+-----------------------------+
           

另外,如果我們将操作應用于

/proc/self/

檔案夾也會得到一些有意思的結果:

ls proc $$ self fd 3 255 引發的一些瑣事

第一行我們已經講明白了,但是第二行和第三行怎麼解釋呢?

man 5 proc

下對這個檔案夾的解釋是這樣的:

/proc/self
              This  directory  refers  to  the  process  accessing  the  /proc
              filesystem, and is identical to the /proc directory named by the
              process ID of the same process.

           

也就是說,

/proc/self/

反應的是目前通路檔案的程序的狀态資料 ,是以我們用

ls /proc/self/fd/

實際上是

ls /proc/${PID of ls}/fd/

,而

ls

會打開這個檔案夾(同時獲得3這個檔案描述符),是以就會看到0,1,2,3這個四個檔案了。但如果我們直接

ls /proc/self/fd/3

,這個時候

ls

的程序還沒有獲得3這個描述符,就嘗試去打開3這個不存在的檔案,是以就報錯了。在CentOS的文檔中提到了這個檔案夾的作用:

The /proc/self/ directory is a link to the currently running process. This allows a process to look at itself without having to know its process ID.
           

另外提一下bash程序中的255檔案描述符,這個是bash獨有的一個小“trick”,其對應的檔案是一個終端裝置:

ls proc $$ self fd 3 255 引發的一些瑣事

這次碰到的問題抽象點說就是擷取資訊的手段本身會影響資訊,這樣的問題在很多地方都有展現,簡單的例如用

ps aux | wc

通過行數來擷取程序數,但

ps aux

本身在運作的時候就會形成一個程序,以後需要注意;)

參考:

  1. An introduction to bash completion
  2. What is the use of file descriptor 255 in bash process
  3. Coreutils - GNU core utilities