UNIX 系統的使用者和群組管理仰賴 /etc/passwd、/etc/group 和 /etc/shadow 等檔案,它們分別儲存使用者資訊、群組資訊和加密密碼。/etc/passwd 的每一行代表一個使用者,包含使用者名稱、UID、GID、主目錄和登入 shell 等資訊。/etc/group 則記錄群組名稱、GID 和成員列表。/etc/shadow 檔案則儲存加密後的使用者密碼及密碼相關設定,有效提升系統安全性。理解這些檔案的結構和作用對於系統管理至關重要。除了使用者和群組管理,UNIX 也提供 I/O 系統呼叫和函式庫來處理輸入輸出操作,並透過緩衝機制提升效率。Shell 作為使用者與系統的介面,提供命令執行和指令碼編寫功能,方便系統管理和自動化任務。C 語言程式在 UNIX 中以程式的形式執行,每個程式都有其生命週期和記憶體佈局,並可透過命令列引數和環境變數與系統互動。更深入的瞭解封裝含程式執行模式、終止方式以及記憶體組態等,有助於開發者編寫更有效率且安全的程式。
UNIX 使用者與群組管理
在 UNIX 系統中,使用者和群組的管理是維護系統安全性和存取控制的重要部分。這些管理機制主要透過 /etc/passwd、/etc/group 和 /etc/shadow 等檔案來實作。以下將詳細介紹這些檔案的格式、功能及其在實際應用中的作用。
機制概述
UNIX 系統透過以下幾個關鍵檔案來管理使用者和群組:
- /etc/passwd:儲存使用者的基本資訊。
- /etc/group:儲存群組的基本資訊。
- /etc/shadow:儲存使用者的加密密碼及密碼相關設定。
這些檔案共同作用,確保系統中的使用者和群組管理既安全又高效。
/etc/passwd 檔案
/etc/passwd 檔案包含每個使用者的基本資訊,每一行代表一個使用者。以下是其格式:
username:x:userID:groupID:comment:home_directory:shell
- username:使用者名稱。
- x:密碼欄位,通常設定為
x,實際密碼儲存在/etc/shadow中。 - userID:使用者識別碼(UID)。
- groupID:主要群組識別碼(GID)。
- comment:使用者的描述或備註。
- home_directory:使用者的主目錄。
- shell:使用者的登入 shell。
例如:
smithj:x:1001:1001:John Smith:/home/smithj:/bin/bash
內容解密:
smithj是使用者名稱。x表示密碼儲存在/etc/shadow中。1001是使用者識別碼(UID)。1001是主要群組識別碼(GID)。John Smith是對該使用者的描述或備註。/home/smithj是該使用者的主目錄。/bin/bash是該使用者的登入 shell。
/etc/group 檔案
/etc/group 檔案包含每個群組的基本資訊,每一行代表一個群組。以下是其格式:
groupname:password:groupID:user-list
- groupname:群組名稱。
- password:通常設定為空,不使用密碼進行群組存取控制。
- groupID:群組識別碼(GID)。
- user-list:屬於該群組的所有使用者名稱,以逗號分隔。
例如:
developers:x:2001:alice,bob,charlie
內容解密:
developers是群組名稱。x表示不使用密碼進行群組存取控制。2001是群組識別碼(GID)。alice,bob,charlie是屬於該群組的所有使用者名稱。
/etc/shadow 檔案
/etc/shadow 檔案儲存每個使用者的加密密碼及相關設定,以增強安全性。每一行代表一個使用者。以下是其格式:
username:encrypted_password:last_changed:min_days:max_days:warn_days:inactive_days:expire_date
各欄位詳細說明如下:
- username:使用者名稱。
- encrypted_password:加密後的密碼。
- last_changed:自 1970 年 1 月 1 日起經過的天數,表示上次更改密碼的時間。
- min_days:最小天數間隔,表示兩次更改密碼之間最少需要多少天。
- max_days:最大天數間隔,表示密碼有效期限,超過此期限後需要更改密碼。
- warn_days:警告天數,表示在密碼過期之前多少天開始警告使用者需要更改密碼。
- inactive_days:不活躍天數,表示在密碼過期後多少天內停用帳號。
- expire_date:停用日期,自 1970 年 1 月 1 日起經過的天數,表示帳號將被停用的日期。
例如:
smithj:Ep6mckrOLChF.:10063:0:99999:7:::
內容解密:
username:smithj
encrypted_password:Ep6mckrOLChF.
last_changed:自 1970 年 1 月 1 日起經過了 10,063 天。
min_days:無最小天數限制。
max_days:99,999 天後需要更改密碼。
warn_days:在密碼過期前 7 天開始警告。
inactive_days:無不活躍天數限制。
expire_date:無具體停用日期設定。
UNIX I/O 操作
UNIX 提供多種方法來進行輸入/輸出操作。以下是兩種主要方法:
- I/O System Calls - 處理核心級別的輸入/輸出操作,如讀取和寫入檔案、網路連線等。
- Library Functions - 提供高層次的 API,簡化了 I/O 操作,通常會轉換為對應的系統呼叫。
I/O Buffering
UNIX 提供函式庫函式來緩衝讀取和寫入資料。這些緩衝區能夠避免頻繁存取外部裝置,從而提高輸入和輸出操作的效率。例如:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return EXIT_FAILURE;
}
setbuf(file, NULL); // Disable buffering for the file
fprintf(file, "Hello, World!\n");
fclose(file);
return EXIT_SUCCESS;
}
小段落標題:程式邏輯與設計考量
這段程式碼展示瞭如何開啟一個檔案並停用其緩衝區。首先,程式嘗試開啟名為 “example.txt” 的檔案以寫入模式。如果開啟失敗,則列印錯誤訊息並離開。接著,程式透過 setbuf() 函式停用檔案的緩衝區功能。最後,將字串 “Hello, World!” 寫入檔案並關閉檔案。
這種做法可以確保資料立即寫入檔案而不會被緩衝區暫時保留。這在某些情況下非常重要,例如在需要即時更新或避免資料丟失時。
UNIX Shell 操作環境
在完成了程式和檔案管理後,接下來將介紹 UNIX Shell 操作環境。Shell 提供了使用者與系統互動的一個介面,允許使用者執行命令、管理程式和檔案等操作。
UNIX Shell 基本概念
Shell 是一種命令直譯器,它可以理解並執行使用者輸入的命令。常見的 Shell 包括 Bash、Zsh 和 Csh 等。Shell 提供了一系列內建命令和指令碼編寫功能,使得系統管理和自動化任務變得更加方便。
Shell Script 基本範例
以下是一個簡單的 Shell 指令碼範例:
#!/bin/bash
echo "Hello, World!"
# Check if a directory exists
if [ -d "/path/to/directory" ]; then
echo "Directory exists."
else
echo "Directory does not exist."
fi
小段落標題:Shell 指令碼設計與邏輯
這段指令碼展示瞭如何在 Bash Shell 中編寫簡單指令碼。首先,指令碼指定了所需執行環境為 Bash (#!/bin/bash) 。接著列印 “Hello, World!” 。然後透過條件陳述式檢查特定目錄是否存在 ([ -d "/path/to/directory" ]) ,並根據結果列印相應訊息。
這樣的一個指令碼可以幫助自動化檢查目錄存在性並根據結果執行相應操作。這是 Shell 指令碼的一個典型應用場景之一。
獨立執行 UNIX 作業系統中的過程
在 UNIX 作業系統中,過程(process)是指執行中的 C 語言程式。每當一個 C 語言程式被執行時,它都會呼叫 main 函式。這個函式的原型如下:
int main(int argc, char *argv[]);
在這個原型中,argc 是命令列引數的數量,而 argv 是指向命令列引數的指標陣列。這些引數由核心透過 exec 函式傳遞給程式,然後再由一個特別的啟動例程呼叫 main 函式。
過程的執行模式
硬體可以觀察到兩種系統執行模式:
-
使用者模式:當使用者過程執行時,它處於使用者模式。在這種模式下,過程只能存取自己的指令和資料,而不能存取內核的指令和資料或其他過程的資料。
-
核心模式:當使用者過程呼叫系統函式時,執行模式會從使用者模式切換到核心模式。核心會處理使用者的請求。在核心模式下,過程可以存取內核和使用者的地址。
過程的終止
當一個過程終止時,作業系統會釋放所有分配給該過程的資源、更新與該過程相關的統計資料,並通知所有相關的過程該過程已經終止。在 UNIX 中,過程可能正常或異常地終止。當一個過程終止時,它會進入殭屍狀態(zombie state),即不釋放程式識別碼(PID),而是等待父程式確認子程式已經終止並釋放 PID。
正常終止
正常終止是指從 main 函式傳回或呼叫 exit() 或 _exit() 函式來結束程式。exit() 函式在離開之前會進行清理操作,例如關閉所有開啟的檔案並重新整理所有緩衝區。其原型如下:
void exit(int status);
而 _exit() 函式則不會進行任何清理操作。
異常終止
當發生以下情況之一時,程式會異常終止:
- 資源使用超限
- 工作不再需要
- 父程式離開
- 其他程式(特別是父程式)呼叫了
abort()函式
異常終止時,核心會生成終止狀態。父程式可以使用 wait() 系統呼叫來取得這個終止狀態。
命令列引數與環境變數
在 C 語言中,main() 函式有兩個標準引數:argc 和 argv。這些引數稱為命令列引數。argc 指定引數的數量(包括命令名稱本身),而 argv 是字串指標的陣列。
int main(int argc, char *argv[])
首先成員 argv[0] 指向命令名稱,第二個成員 argv[1] 指向第一個命令引數。
由於有了 argv,這些引數使得程式能夠輕鬆地根據使用者輸入的命令列引數進行反應。例如,您可能希望您的程式檢測到單詞「help」作為第一個引數並在標準輸出上傳送幫助檔案。此外,檔案名稱也可以透過命令列引數傳遞並用於開啟宣告中。
環境變數
環境變數由 envp 表示為一個字串陣列,格式為 VAR=value。例如:PATH=/bin;/usr/bin。最後一個陣列元素是一個空指標。這使得 C 語言編寫者可以存取 Shell 的全域性環境。
除了 envp 向量之外,還可以透過呼叫 getenv() 操作環境變數。以下是如何使用它來存取 Shell 環境變數 $HOME 的例子:
char *string;
string = getenv("HOME");
此呼叫取得 $HOME 變數的值並將其儲存在字串中。
範例
以下是一個展示如何處理命令列引數和環境變數的範例:
/* myprog.c */
#include <stdio.h>
int main(int argc, char *argv[], char *envp[]) {
int i;
printf("The program %s has %d arguments\n", argv[0], argc);
printf("The first two arguments are %s and %s\n", argv[1], argv[2]);
for (i = 0; envp[i]; i++) {
printf("%s\n", envp[i]);
}
}
在這個範例中,當我們執行以下命令時:
$ ./myprog this is good
輸出結果將是:
The program ./myprog has 4 arguments
The first two arguments are this and is
PWD=/home/user
HOME=/home/user
...
內容解密:
上述範例中的函示中:
- 主函示中的引數:函示接收三個引數:命令列引數計算值 (
argc) 、命令列引數 (argv) 和環境變數 (envp)。 - 命令列引數處理:首先輸出了命令列引數計算值和前兩個引數值。
- 環境變數迴圈:利用迴圈遍歷所有環境變數並列印每一個環境變數。
UNIX 過程記憶體佈局
分配給 UNXI 作業系統中的記憶體分割槽通常被分割成幾個特定區域:
- 碼區(Code area):這部分通常被稱為 UNIX 中的文字區域(text area),包含可執行碼。如果有多個過程執行相同的碼區域程式碼將是相同且僅有一份。
- 資料區(Data area):包含被處理資料和初始化以及未初始化資料變數。
- 堆積疊區(Stack area):自動產生且在執行期間調整大小以適應函示呼叫所需空間。
- 堆積疊區(Heap):虛擬位址空間的一部分包含動態分配給過程使用之變數。
跳轉到另一函示
C 語言允許跳轉到設定在另一函示中的標籤位置。要將控制權轉移到另一函示需要使用函式庫宏 setjmp() 和 longjmp()。一般來說應避免使用這類別跳轉因為不是良好的編寫習慣。
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
內容解密:
此宏用於儲存目前環境至變數 env, 第一次回傳值為0;第二次回傳由 longjmp() 的第二引數設定之值。
範例如下:
#include <setjmp.h>
#include <stdio.h>
jmp_buf buffer;
void foo() {
longjmp(buffer, 1);
}
int main() {
if (setjmp(buffer) == 0) {
printf("First call\n");
foo();
} else {
printf("Second call\n");
}
}
內容解密:
- setjmp() 和 longjmp() 的運用:先呼叫 setjmp() 在 buffer 儲存目前環境(第一次回傳為0),接著再呼叫 foo() 再進行 longjmp(buffer,1) 跳轉控制權再回到 setjmp() 第二次回傳為1後即可顯示 “Second call” 。
- 非建議編寫習慣:因為可能使得流控制更加難以追蹤與理解故不建議使用。