前言
PHP 與作業系統互動、檔案系統操作是後端開發核心能力。本文系統整理 `exec` 族函數、檔案鎖、目錄掃描、串流處理、臨時檔案等高頻函數,解析參數差異、安全風險與最佳實踐。
一、系統指令執行函數族
對照表:三大執行函數核心差異
| 函數 | 回傳值 | 輸出處理 | 適用場景 | 阻塞性 |
| exec() | 最後一行輸出 (string) | 可選陣列參數接收所有行 | 需捕獲結果碼、簡單指令 | 阻塞 |
| system() | 最後一行輸出 | 直接輸出到標準輸出 | 即時顯示輸出、安裝腳本 | 阻塞 |
| passthru() | 無 (void) | 原始二進位直接輸出 | 圖片/PDF/二進位串流 | 阻塞 |
| shell_exec() / `反引號` | 完整輸出 (string) | 捕獲所有輸出 | 需完整輸出字串處理 | 阻塞 |
| proc_open() | 資源控制代碼 | 雙向管道、非阻塞可控 | 複雜互動、長時間程序 | 可非阻塞 |
參數詳解
// exec(string $command, array &$output = null, int &$return_var = null): string
$output = [];
$returnVar = 0;
$lastLine = exec("ls -la /tmp", $output, $returnVar);
// $output = ["total 12", "drwxrwxrwt 2 root root 4096 Jan 15 10:00 .", ...]
// $returnVar = 0 (成功)
// system(string $command, int &$return_var = null): string
$lastLine = system("ls -la /tmp", $returnVar); // 直接輸出到瀏覽器/終端
// passthru(string $command, int &$return_var = null): void
header("Content-Type: image/jpeg");
passthru("/usr/bin/convert /path/to/input.png jpeg:-"); // 圖片轉檔直接輸出
安全關鍵:escapeshellcmd / escapeshellarg
$userInput = $_GET['filename'] ?? '';
// 錯誤:exec("ls $userInput"); // 注入風險:"; rm -rf /"
// 正確:參數級跳脫
$safeArg = escapeshellarg($userInput); // 單引號包裹並跳脫內部單引號
exec("ls {$safeArg}");
// 或指令級跳脫 (較不推薦,僅跳脫特殊字元)
$safeCmd = escapeshellcmd("ls {$userInput}");
exec($safeCmd);
二、檔案鎖定:flock
bool flock(resource $stream, int $operation, int &$wouldblock = null): bool
操作常數 (鎖定模式)
| 常數 | 數值 | 說明 |
| LOCK_SH | 1 | 共享鎖 (讀鎖),多程序可同時持有 |
| LOCK_EX | 2 | 獨佔鎖 (寫鎖),同一時間僅一程序 |
| LOCK_UN | 3 | 釋放鎖 |
| LOCK_NB | 4 | 非阻塞模式 (搭配 LOCK_SH\|LOCK_EX 使用) |
經典寫入模式 (獨佔鎖 + 非阻塞重試)
function safeWrite(string $file, string $data): bool {
$fp = fopen($file, 'c+'); // 開啟讀寫,不截斷
if (!$fp) return false;
$retry = 0;
while (!flock($fp, LOCK_EX | LOCK_NB)) {
if (++$retry > 10) { fclose($fp); return false; }
usleep(10000); // 10ms 重試
}
ftruncate($fp, 0); // 截斷
fwrite($fp, $data);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
三、目錄掃描與檔案資訊
opendir / readdir / closedir (底層迭代器)
$dir = opendir("/var/www/html");
if ($dir) {
while (($file = readdir($dir)) !== false) {
if ($file !== '.' && $file !== '..') {
echo $file . "\n";
}
}
closedir($dir);
}
// 現代替代:DirectoryIterator (SPL, 物件導向)
foreach (new DirectoryIterator("/var/www/html") as $fileInfo) {
if (!$fileInfo->isDot()) echo $fileInfo->getFilename() . "\n";
}
chdir / getcwd - 工作目錄切換
echo getcwd(); // /current/path
chdir("/tmp"); // 切換
echo getcwd(); // /tmp
磁碟空間與檔案屬性
| 函數 | 功能 | 範例回傳 |
| disk_free_space($dir) | 目錄可用空間 (bytes) | 10737418240 (10GB) |
| disk_total_space($dir) | 總空間 | 53687091200 (50GB) |
| filesize($file) | 檔案大小 | 1024 |
| fileperms($file) | 權限 (八進位) | 0100644 (需用 substr(sprintf('%o', $p), -4)) |
| fileowner / filegroup | 擁有者 UID/GID | 1000 / 1000 |
| filetype($file) | 類型 | "file", "dir", "link", "fifo", "block", "char", "unknown" |
| is_readable / is_writable / is_executable / is_file / is_dir / is_link | 布林檢查 | true/false |
檔案時間戳
fileatime($file) // 最後存取時間
filemtime($file) // 最後修改時間 (最常用)
filectime($file) // 最後狀態變更時間 (inode 變更)
touch($file) // 更新 atime/mtime 為現在 (或指定時間)
touch($file, time() - 86400); // 設為 1 天前
四、檔案串流操作 (資源控制代碼)
fopen 模式速查
| 模式 | 讀 | 寫 | 建立 | 截斷 | 指標位置 | 備註 |
| r | ✓ | | | | 開頭 | 檔案必須存在 |
| r+ | ✓ | ✓ | | | 開頭 | 檔案必須存在 |
| w | | ✓ | ✓ | ✓ | 開頭 | 覆蓋現有 |
| w+ | ✓ | ✓ | ✓ | ✓ | 開頭 | 覆蓋現有 |
| a | | ✓ | ✓ | | 結尾 | 追加模式 |
| a+ | ✓ | ✓ | ✓ | | 結尾 | 追加可讀 |
| c | | ✓ | ✓ | | 開頭 | 不截斷 (PHP 5.2+) |
| c+ | ✓ | ✓ | ✓ | | 開頭 | 不截斷,推薦寫入用 |
| x | | ✓ | ✓ | | 開頭 | 獨佔建立,存在則失敗 |
核心讀寫函數
$fp = fopen("data.txt", "c+");
// 讀取
fgets($fp, 1024) // 讀一行 (含換行),長度限制
fgetss($fp, 1024) // 讀一行並去除 HTML/PHP 標籤
fgetc($fp) // 讀單一字元
fread($fp, 8192) // 讀指定位元組 (二進位安全)
stream_get_contents($fp) // 讀取剩餘所有 (PHP 5+)
// 寫入
fwrite($fp, $data) // 寫入字串 (回傳寫入位元組數)
fputs($fp, $data) // fwrite 別名
fprintf($fp, "%s\n", $var) // 格式化寫入
// 指標控制
fseek($fp, 0, SEEK_SET) // 移至開頭
fseek($fp, -10, SEEK_END) // 移至倒數 10 位元組
ftell($fp) // 取得目前位置
rewind($fp) // 等同 fseek($fp, 0)
// 狀態檢查
feof($fp) // 是否到達 EOF
fflush($fp) // 強制刷新緩衝區到磁碟
// 關閉
fclose($fp);
高層便利函數
file_get_contents("file.txt") // 一次讀全檔 (字串)
file("file.txt") // 讀全檔為陣列 (每行一元素)
readfile("file.txt") // 直接輸出到標準輸出 (回傳位元組數)
file_put_contents("file.txt", $data, FILE_APPEND | LOCK_EX) // 原子寫入 (PHP 5+)
tempnam("/tmp", "prefix") // 建立唯一暫存檔名
tmpfile() // 建立匿名暫存檔 (關閉/腳本結束自動刪除)
五、實戰:安全下載大檔案 (串流輸出 + 斷點續傳)
function streamDownload(string $filePath, string $fileName = '') {
if (!is_file($filePath)) { http_response_code(404); return; }
$fileName = $fileName ?: basename($filePath);
$size = filesize($filePath);
$fp = fopen($filePath, 'rb');
// HTTP 標頭
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Content-Length: ' . $size);
header('Accept-Ranges: bytes');
header('Cache-Control: no-cache, must-revalidate');
// 斷點續傳支援
if (isset($_SERVER['HTTP_RANGE'])) {
list($unit, $range) = explode('=', $_SERVER['HTTP_RANGE']);
if ($unit === 'bytes') {
list($start, $end) = explode('-', $range);
$start = (int)$start;
$end = $end ? (int)$end : $size - 1;
if ($start < $size && $end < $size) {
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
header('Content-Length: ' . ($end - $start + 1));
fseek($fp, $start);
}
}
}
// 串流輸出 (避免記憶體爆炸)
while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
echo fread($fp, 8192);
flush();
}
fclose($fp);
}
總結
系統執行首選 `proc_open` 掌控雙向管道,簡單場景用 `exec` 捕獲輸出,**務必 `escapeshellarg` 防注入**。檔案操作核心是 **`fopen` 模式選擇 + `flock` 並發控制**,大檔案必用串流 `fread`/`fwrite` 避免記憶體耗盡。現代專案目錄掃描推薦 SPL `DirectoryIterator`/`RecursiveDirectoryIterator`,檔案讀寫優先 `file_get_contents`/`file_put_contents` (支援 `LOCK_EX` 原子寫入)。理解這些底層機制,才能寫出高效、安全、可靠的系統級 PHP 程式。