自己鍵入並執行了下本章的程式碼
p1.c
程式碼:
執行結果:
書上的解釋:
補充:
fork()
是用於建立子程序的系統呼叫。呼叫 fork()
後,會在父程序中建立一個與自己幾乎完全相同的子程序,父程序和子程序從 fork()
之後的程式碼開始並行執行。fork()
是 UNIX 系統中實現併發操作的一個基礎函式。
fork() 的返回值:
-
對於父程序:fork() 返回子程序的 PID(程序 ID),這個值是一個正整數。
-
對於子程序:fork() 返回 0。
-
如果 fork() 呼叫失敗:返回 -1,此時不會建立子程序。
透過判斷 fork() 的返回值,可以區分當前程序是父程序還是子程序,並讓它們執行不同的程式碼。
p2.c
程式碼:
執行結果:
書上的解釋:
補充:
wait()
函式用於使父程序暫停執行,直到它的一個子程序結束。這個函式會收集子程序的退出狀態,以便父程序處理。
wait() 的功能:
-
等待子程序:
wait()
會使父程序暫停,直到有一個子程序結束(使父程序等待其任意一個子程序的結束)。如果父程序沒有子程序,wait()
立即返回 -1。 -
返回子程序的 PID:
wait()
返回終止的子程序的 PID。 -
儲存退出狀態:
wait()
的引數是一個指標,它用於儲存子程序的退出狀態。如果父程序不關心退出狀態,可以傳遞 NULL,即使用wait(NULL)
。
wait() 的返回值:
-
成功時:返回終止的子程序的 PID,並將子程序的退出狀態儲存在傳入的指標中。
-
失敗時:返回 -1,通常是因為當前沒有可等待的子程序。
wait() 與子程序的退出狀態:
wait()
可以透過指標引數獲得子程序的退出狀態,一般透過 WIFEXITED(status)
和 WEXITSTATUS(status)
來檢查和獲取退出碼:
-
WIFEXITED(status)
:判斷子程序是否正常退出。 -
WEXITSTATUS(status)
:獲取子程序的退出碼(在子程序中呼叫exit()
或return
返回的值)。
比如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("Fork failed");
exit(1);
}
else if (pid == 0)
{
// 子程序
printf("This is the child process. PID: %d\n", getpid());
exit(42); // 子程序退出並返回 42
}
else
{ // 父程序
int status;
pid_t child_pid = wait(&status); // 等待子程序結束
if (WIFEXITED(status))
{ // 判斷子程序是否正常退出
printf("Child process %d exited with status %d\n", child_pid, WEXITSTATUS(status));
}
}
return 0;
}
輸出如下:
附上關於perror
和fprintf
的一篇部落格:
perror和fprintf有什麼區別
更多的:
如果想閱讀man手冊來獲取wait()
的更多細節,可使用man 2 wait
命令。
輸入該命令並enter後,出現以下內容:
p3.c
程式碼
執行結果:
書上的解釋:
exec()
系統呼叫,它也是建立程序 API 的一個重要部分。這個系統呼叫可以讓子程序執行與父程序不同的程式。例如,在 p2.c 中呼叫 fork()
,這只是在你想執行相同程式的複製誰有用。但是,我們常常想執行不同的程式,exec()
正好做這樣的事。
補充:
exec()
系列函式用於在當前程序中執行一個新的程式。呼叫 exec()
後,當前程序的映像被替換為新程式的映像,因此,exec()
成功後不會返回到原來的程式。
功能:
-
替換當前程序:
exec()
會用指定的程式替換當前程序的上下文,包括程式碼段、資料段和堆疊。此後,程序執行的新程式程式碼。 -
不返回:如果
exec()
呼叫成功,後續程式碼不會執行;如果失敗,會返回到呼叫的位置,通常會產生一個錯誤。
exec()系列函式:
用man 3 exec
檢視更多內容
附上一篇部落格:exec()系列函式
p4.c
程式碼
執行結果:
書上的解釋:
補充:
-
STDOUT_FILENO
是什麼STDOUT_FILENO
是一個常量,通常在 C/C++ 程式設計中用於表示標準輸出流的檔案描述符。在 POSIX 系統(如 Linux 和 macOS)中,標準輸出的檔案描述符通常是 1。這個常量在<unistd.h>
標頭檔案中定義。使用
STDOUT_FILENO
可以使程式碼更具可讀性,避免硬編碼數字。例如,使用write(STDOUT_FILENO, "Hello, World!\n", 14);
來向標準輸出寫入資料,而不是直接使用write(1, "Hello, World!\n", 14);
。 -
POSIX是什麼
POSIX(可移植作業系統介面,Portable Operating System Interface)是一個由 IEEE(電氣和電子工程師協會)制定的標準,旨在促進不同作業系統之間的相容性和可移植性。
POSIX 定義了一系列作業系統 API(應用程式程式設計介面)、命令列工具和shell介面,確保程式可以在遵循 POSIX 標準的多個作業系統上執行,而不需要進行大幅修改。
許多現代作業系統(如 Linux、macOS 和一些 UNIX 變種,但windows不完全遵循)都遵循 POSIX 標準,因此開發者可以更容易地編寫跨平臺的程式碼。
-
S_IRWXU是什麼
S_IRWXU 是一個常量,用於指定檔案許可權位中的使用者許可權。在 C/C++ 程式設計中,尤其是涉及到 POSIX 系統的檔案操作時,檔案許可權通常以位掩碼的形式表示。
定義:
S_IRWXU 是一個八進位制值,通常等於 0700,表示使用者(檔案的所有者)對檔案的讀、寫和執行許可權。
各許可權的具體值
-
S_IRUSR:讀許可權(0400)。
-
S_IWUSR:寫許可權(0200)。
-
S_IXUSR:執行許可權(0100)。
組合:
將這些許可權合併起來:
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR = 0400 | 0200 | 0100 = 0700(八進位制)。
S_IRWXU 實際上是這三個許可權的組合,表示使用者可以:
-
讀檔案。
-
寫檔案。
-
執行檔案(如果是可執行檔案)。
-
作業
第一題
問題:
編寫一個呼叫 fork()的程式。在呼叫 fork()之前,讓主程序訪問一個變數(例如 x)並將其值設定為某個值(例如 100)。子程序中的變數有什麼值?當子程序和父程序都改變x 的值時,變數會發生什麼?
自己寫的
輸出如下:
子程序的變數一開始與父程序相同。但是實際上父子程序中的同名變數已經不在同一記憶體區域,實際上是兩個變數,因此父子程序的變數值改變不會影響另外一個程序。
書上原話:
子程序並不是完全複製了父程序。具體來說,雖然它擁有自己的
地址空間(即擁有自己的私有記憶體)、暫存器、程式計數器等,但是它從 fork()返回的值是不同的。
說明子程序的記憶體區域已經和父程序的記憶體區域不同了,在記憶體中重新開闢了一個空間給子程序。
第二題
問題:
編寫一個開啟檔案的程式(使用 open()系統呼叫),然後呼叫 fork()建立一個新程序。子程序和父程序都可以訪問 open()返回的檔案描述符嗎?當它們併發(即同時)寫入檔案時,會發生什麼?
自己寫的
輸出如下:
而且自動建立了一個2.txt
檔案,內容為:
子程序和父程序都可以訪問fd(fd是該程式中的一個檔案描述符),但是存在競爭,無法同時使用fd,但最終都會寫入成功。
第三題
問題:
使用 fork()編寫另一個程式。子程序應列印“hello”,父程序應列印“goodbye”。你應該嘗試確保子程序始終先列印。你能否不在父程序呼叫 wait()而做到這一點呢?
自己寫的
輸出如下:
使用vfork()
函式,可以在子程序結束後再執行父程序。
補充:
為什麼使用vfork()
函式,可以在子程序結束後再執行父程序?
使用 vfork()
時,子程序會與父程序共享地址空間,直到子程序呼叫 exec()
或 _exit()
或exit()
(但是對於 _exit()
和exit()
一般vfork()
是和_exit()
搭配使用,exit()
在此處不建議使用,具體的原因請看這篇部落格C中的open(), write(), close(), fopen(), exit(), _exit())。這是實現子程序執行結束後再執行父程序的原因。以下是具體的解釋:
工作機制
-
共享地址空間:
當呼叫
vfork()
時,子程序並不會複製父程序的整個地址空間,而是共享它。這樣可以節省記憶體開銷。 -
執行順序:
-
子程序被建立後,它會在父程序的上下文中執行。
-
子程序可以在其執行期間直接修改父程序的變數,但這通常是不安全的,因此應避免。
-
子程序在完成其任務後,必須呼叫
exec()
(替換為新程式)或_exit()
(終止自己)來結束其執行。只有在此之後,父程序才能繼續執行。
-
阻塞行為:
vfork()
會使父程序阻塞,直到子程序結束。這意味著父程序在子程序執行期間不會繼續執行。這個機制確保了在子程序完成其任務之前,父程序不會訪問可能被修改的共享記憶體。
第四題
問題:
編寫一個呼叫 fork()的程式,然後呼叫某種形式的 exec()來執行程式/bin/ls。看看是否可以嘗試 exec()的所有變體,包括 execl()、execle()、execlp()、execv()、execvp()和 execvP()。為什麼同樣的基本呼叫會有這麼多變種?
自己寫的
輸出如下:
這個其實是隻執行execl(cmd, "ls", NULL)
的結果,因為exec系列函式不會返回,因此後面的程式碼不會執行。
我們可以透過註釋不要的語句的的方式來檢視其他的結果,此處不展示了。
可以看這篇部落格exec()系列函式
同樣的基本呼叫會有這麼多變種是為了適應不同的呼叫形式和環境要求。
第五題
問題:
現在編寫一個程式,在父程序中使用 wait(),等待子程序完成。wait()返回什麼?如果你在子程序中使用 wait()會發生什麼?
自己寫的
輸出如下:
對於父程序:fork()
返回子程序的 PID,對於子程序:fork()
返回0。
父程序使用wait()
返回終止的子程序的 PID,子程序本身沒有子程序,所以返回-1。
第六題
問題:
對前一個程式稍作修改,這次使用 waitpid()而不是 wait()。什麼時候 waitpid()會有用?
自己寫的
輸出如下:
對
waitpid
做個介紹
waitpid
是一個用於程序管理的系統呼叫,用於等待特定子程序的狀態改變,通常是等待子程序結束。它提供了比 wait 更靈活的功能,允許你指定要等待的程序。
-
函式原型:
pid_t waitpid(pid_t pid, int *status, int options);
-
pid:要等待的程序的程序ID。如果為 -1,則等待任何子程序。如果為 0,則等待與呼叫程序同組的任意子程序。如果為正數,則等待指定的程序ID。
-
status:指向整數的指標,用於儲存子程序的退出狀態資訊。如果不需要此資訊,可以傳遞 NULL。
-
options:可以為0,或者設定一些選項,例如:
-
WNOHANG:非阻塞模式,如果沒有子程序結束,則立即返回。
-
WUNTRACED:也返回那些被暫停的子程序的狀態。
-
-
-
返回值
返回值是結束的子程序的程序ID。如果呼叫失敗,返回 -1,並設定 errno。
-
舉一個例子
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子程序執行 printf("Child process running...\n"); sleep(2); // 模擬一些工作 exit(42); // 返回一個退出狀態 } else { // 父程序等待子程序 int status; pid_t result = waitpid(pid, &status, 0); if (result == -1) { perror("waitpid failed"); exit(EXIT_FAILURE); } // 檢查子程序的退出狀態 if (WIFEXITED(status)) { printf("Child exited with status: %d\n", WEXITSTATUS(status)); } } return 0; }
輸出如下:
其中
WIFEXITED(status)
和WEXITSTATUS(status)
是用於檢查子程序的退出狀態的宏。
第七題
問題:
編寫一個建立子程序的程式,然後在子程序中關閉標準輸出(STDOUT_FILENO)。如果子程序在關閉描述符後呼叫 printf()列印輸出,會發生什麼?
自己寫的
輸出如下:
子程序裡的printf語句沒有列印出來。
第八題
問題:
編寫一個程式,建立兩個子程序,並使用 pipe()系統呼叫,將一個子程序的標準輸出連線到另一個子程序的標準輸入。
自己寫的
有點長,截不全,就不截圖了,直接給出程式碼
/* ************************************************************************
> File Name: 8.c
> Author: whq
> Created Time: 2024年10月30日 星期三 21時24分55秒
> Description: 《作業系統導論》第五章第八題
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int fd[2];
int p = pipe(fd);
if (p < 0)
{
perror("pipe failed\n");
exit(1);
}
int i = 0;
int rc[2];
for (i = 0; i < 2; i ++ )
{
rc[i] = fork();
if (rc[i] < 0)
{
perror("fork failed\n");
exit(1);
}
else if(rc[i] == 0)
{
switch(i) {
case 0:
{
printf("I am child0 (pid:%d)\n", getpid());
char *msg = "hello, I am child0";
close(fd[0]);
write(fd[1], msg, sizeof(char) * strlen(msg));
return 1;
}
case 1:
{
printf("I am child1 (pid:%d)\n", getpid());
char asw[20];
close(fd[1]);
int res = read(fd[0], asw, sizeof(char) * 20);
printf("I am child1 and I get msg (size:%d, %s) from child0\n", res, asw);
return 2;
}
}
break;
}
else
{
int wc = waitpid(rc[i], NULL, 0);
printf("I am parent (pid:%d) of (pid:%d)\n", getpid(), wc);
}
}
waitpid(rc[1], NULL, 0);
return 0;
}
輸出如下:
補充一下pipe的使用
pipe
函式用於在Unix/Linux系統中建立一個管道,它提供了一種程序間通訊(IPC)機制,使得一個程序可以透過管道向另一個程序傳遞資料。管道是一個緩衝區,具有讀寫兩端,資料寫入管道一端後,可以從另一端讀取。
-
函式原型
int pipe(int pipefd[2]);
pipefd
:一個長度為2的整型陣列,用於儲存管道的檔案描述符。pipefd[0]
是讀端的檔案描述符,pipefd[1]
是寫端的檔案描述符。
-
返回值
如果成功,返回 0;如果失敗,返回 -1,並設定 errno。
-
舉一個例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe failed");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子程序:關閉寫端,讀取父程序傳送的資料
close(pipefd[1]); // 關閉寫端
char buffer[100];
read(pipefd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(pipefd[0]); // 關閉讀端
exit(0);
} else {
// 父程序:關閉讀端,寫資料到子程序
close(pipefd[0]); // 關閉讀端
const char *message = "Hello from parent!";
write(pipefd[1], message, strlen(message) + 1); // +1 以包括 NULL 結束符
close(pipefd[1]); // 關閉寫端
wait(NULL); // 等待子程序結束
}
return 0;
}
輸出如下: