
在維護實習機關伺服器的過程中,偶然發現一個有350萬檔案的檔案夾需要清理,于是我習慣性執行了
rm -rf ./*
,卻在數秒後被告知“參數清單過長”。在一番折騰過後,我終于通過取巧的辦法完成了這一任務,也随着相關核心源碼的閱讀,了解到了關于Linux Shell的一些有趣特性。
本文原載于未命名小站,由作者本人同步至知乎,轉載請注明原作者部落格位址或本連結,謝謝!
0x01
最初我以為是
rm
指令對檔案數量存在限制,但當我嘗試
ls ./*
、
touch ./*
等指令都遇到這一問題之後,我開始将注意力放在Bash本身上。也許是通配符的限制。
突然,我想起來
rm
指令支援管道送入參數,也許可以通過這樣的辦法變相完成任務。于是我在另一個目錄做了個測試:
$ touch 123-1
$ touch 123-2
$ echo "123-1 123-2" | xargs rm
這兩個檔案果然消失。
于是我嘗試使用find将目錄下面所有檔案列出:
find . -name "*" | more # 使用more避免350多萬個檔案把終端擠崩潰
find . -name "*" | wc -l # 大緻了解檔案個數
輸出的檔案個數與我通過
ls -l
指令輸出的個數基本一緻,果然輸出了所有檔案。接下來要做的就是将這些檔案送到
rm
中。
0x02
但事實并非如此簡單,當我執行以下指令,以為可以一口氣順利删除所有檔案的時候,我傻眼了:
$ find . -name "*" | xargs rm
rm: log: No such file or directory
rm: 20190601-110204.log: No such file or directory
... # 所有待删除檔案均發生報錯
我重新觀察檔案名,發現檔案名格式均為
log yyyymmdd-hhmmss.log
,衆所周知Bash靠空格分割參數,檔案名被傳入rm的時候照着空格被截斷,成為了兩個檔案名,難怪删除失敗!
0x03
吸取教訓,我使用了一個新的參數:
find . -name "*" -print0 | xargs -0 rm
注意這一參數裡的
-print0
與
-0
,這是為了區分空格與分界符,加入參數後此前用于分隔參數的空格(
0x0a
)則會變成NUL(
0x00
),這一參數的效果可以通過16進制檢視器展現:
$ ls
123 321
123 322
$ find . -name "12*" > 1.log
$ find . -name "12*" -print0 > 2.log
$ hexdump -C 1.log
00000000 2e 2f 31 32 33 20 33 32 31 0a 2e 2f 31 32 33 20 |./123 321../123 |
00000010 33 32 32 0a |322.|
00000014
$ hexdump -C 2.log
00000000 2e 2f 31 32 33 20 33 32 31 00 2e 2f 31 32 33 20 |./123 321../123 |
00000010 33 32 32 00 |322.|
00000014
可以發現在兩個不同輸出模式下,分隔符不一樣。預設的分隔符與空格一緻,即
0x0a
,但當開啟
-print0
模式後,分隔符變成了
0x00
,配合管道接收端的
-0
參數将
NUL
字元正确解析成參數定界符,則可以完成帶空格檔案名的正常解析。
解決了這一問題,我們再次執行,問題随即解決。
0x04
過了兩天,又有一台伺服器需要删除大量檔案,且上司要求隻删除檔案不删除裡面的目錄,我一看,又是400多萬個檔案。但在之前的折騰過程中,我早有準備。
find
指令預設開啟遞歸模式,如果隻删除檔案不删除目錄,隻需要配置遞歸深度為1即可:
find . -maxdepth 1 -name "*" -print0 | xargs -0 rm
執行指令後再執行
ls -l
,發現問題果然解決,所有目錄完好無損。
0x05
快速解決完問題後,我一看離下班還有好一陣子,便開始琢磨Bash内通配符的長度限制到底從哪來。
我首先找了另一個有大量檔案的目錄開始實驗,換用zsh進行
ls ./*
操作,得到的确是一樣結果。看來該長度限制與Bash無關。
我突然想起來曾經看過的一個安全類視訊:Youtube - Bash injection without letters or numbers - 33c3ctf hohoho (misc 350) - LiveOverFlow,其中解釋了通配符(Wildcard)的原理。
當我們輸入
*
的時候,Shell所做的是列舉出滿足通配符規則的所有檔案,并以空格分割,然後送進Shell。舉例而言,如果你有一個全是txt檔案的目錄,你直接執行
*
,就會發現以下錯誤:
$ touch abc.txt
$ touch bbc.txt
$ *
bash: command not found: abc.txt
$ echo *
abc.txt bbc.txt
相信看到這裡,大家都明白通配符的行為以及為什麼上述示例會報錯,在上述示例中,Shell将
abc.txt
看做指令,而将
bbc.txt
看做參數。這也說明了通配符的行為:将滿足條件的檔案輸出為文本(并預設輸出到Shell)。
0x06
當我們繼續向下挖掘,我們會想到Shell執行指令的本質:
exec()
類系統調用。這一限制如果并非來自于Shell(因為
find . -name "*"
并不會報錯),那麼就一定來自于系統調用。而一個Shell指令被執行的第一站則是
exec()
及其六個實際調用:
execl(),execle(),execlp(),execv(),execvp(),execve()
。
于是我們使用
strace
工具來檢查所有系統調用:
$ ls
1234.txt
123.txt
$ strace ls *
execve("/usr/bin/ls", ["ls", "1234.txt", "123.txt"], [/* 28 vars */]) = 0
...
看到這裡,相信讀者已經心裡有數了,我們的指令與參數都被作為
execve()
函數的第二個參數,以數組形式被傳入。考慮到數組預設存儲在棧中,該限制是否來自于Shell對棧空間的限制呢?
我查閱了Linux的源碼,在
fs/exec.c:478
中找到了我要的内容:源碼
static int prepare_arg_pages(struct linux_binprm *bprm,
struct user_arg_ptr argv, struct user_arg_ptr envp)
{
unsigned long limit, ptr_size;
bprm->argc = count(argv, MAX_ARG_STRINGS);
if (bprm->argc < 0)
return bprm->argc;
bprm->envc = count(envp, MAX_ARG_STRINGS);
if (bprm->envc < 0)
return bprm->envc;
/*
* Limit to 1/4 of the max stack size or 3/4 of _STK_LIM
* (whichever is smaller) for the argv+env strings.
* This ensures that:
* - the remaining binfmt code will not run out of stack space,
* - the program will have a reasonable amount of stack left
* to work from.
*/
limit = _STK_LIM / 4 * 3;
limit = min(limit, bprm->rlim_stack.rlim_cur / 4);
/*
* We've historically supported up to 32 pages (ARG_MAX)
* of argument strings even with small stacks
*/
limit = max_t(unsigned long, limit, ARG_MAX);
/*
* We must account for the size of all the argv and envp pointers to
* the argv and envp strings, since they will also take up space in
* the stack. They aren't stored until much later when we can't
* signal to the parent that the child has run out of stack space.
* Instead, calculate it here so it's possible to fail gracefully.
*/
ptr_size = (bprm->argc + bprm->envc) * sizeof(void *);
if (limit <= ptr_size)
return -E2BIG;
limit -= ptr_size;
bprm->argmin = bprm->p - limit;
return 0;
}
從完整的注釋中我們可以得知,限制最大參數長度的參數叫做
ARG_MAX
,而且其大小為棧的
1/4
(可能是為了保證參數以外還有多的空間可以存儲其他資料)。當然,如果你是個考古愛好者,你會發現在2.6版本核心(低于2.6.22)的
include/linux/limits.h
檔案中,
ARG_MAX
是一個寫死的常量 :考古連結。
關于
ARG_MAX
我們可以通過Linux下的
getconf
指令來擷取,這是一個擷取Linux下所有全局變量/常量的指令:
$ getconf ARG_MAX
2097152
我們再查詢目前配置的棧大小:
$ ulimit -s
8192
ARG_MAX
參數的機關是Byte,
ulimit -s
指令的機關是MB,可以看到目前最大參數數量的确是棧空間的
1/4
。那如果我們把棧空間增大呢?
$ ulimit -s 81920
$ ulimit -s
81920
$ getconf ARG_MAX
20971520
可以看到,允許的最大參數數量立馬随着棧空間增大而同步增大。這個時候我們再來删除之前那個大目錄,就不會出現『參數清單過長』的錯誤提示了。
實際上這一限制在大多數現代作業系統中均存在(例如MacOS、Windows等),可參考此處獲得更多資訊:ARG_MAX, maximum length of arguments for a new process