非同步跳轉和程式管理是系統程式設計的根本,理解它們的工作原理對於構建穩健高效的應用至關重要。非同步跳轉提供了一種靈活的控制流程機制,允許程式在特定事件發生時中斷正常的執行順序,跳轉到預先設定的程式碼段繼續執行。這在處理異常、中斷和訊號等場景中非常有用。程式管理則負責作業系統中程式的建立、執行、排程和終止,確保系統資源的合理分配和利用。現代作業系統都採用多工機制,允許多個程式同時執行,程式管理則負責協調這些程式之間的互動,避免衝突和死鎖。深入理解程式狀態、程式控制塊以及程式間通訊等概念,是開發高效可靠的系統軟體的關鍵。在 C 語言中,setjmplongjmp 函式提供了實作非同步跳轉的標準方式,而 forkwaitexec 等函式則構成了程式管理的核心工具。

非同步跳轉與程式管理

在現代程式設計中,非同步跳轉和程式管理是兩個關鍵概念。非同步跳轉機制允許程式在特定條件下跳過正常的執行流程,而程式管理則負責建立、控制和終止程式。本文將探討這兩個主題,並提供具體的例項和技術分析。

非同步跳轉

非同步跳轉是指程式在執行過程中,根據某些條件突然跳到另一個位置繼續執行。這種機制在處理異常情況或需要快速回應的場景中非常有用。

範例:setjmplongjmp

以下是一個使用 setjmplongjmp 的範例,展示如何實作非同步跳轉:

#include <setjmp.h>
#include <stdio.h>

jmp_buf ebuf;

void f(void);
int main(void)
{
    int i;
    printf("1 ");
    i = setjmp(ebuf);
    if(i == 0) {
        f();
    }
    printf("%d", i);
    return 0;
}

void f(void)
{
    printf("2 ");
    longjmp(ebuf, 3);
}

輸出解析

此範例的輸出為:

1
2
3

setjmp 第一次被呼叫時,它會傳回 0 給變數 i,並將控制權交給函式 f()。在函式 f() 中,會列印預出 2,然後透過 longjmpsetjmp 的傳回值設定為 3。這使得程式跳回到 setjmp 陳述式後繼續執行,並列印預出 3

程式狀態

每個程式在其生命週期內會經歷多種狀態,這些狀態可以透過狀態轉換圖來表示。以下是一些主要的程式狀態:

  • 執行:程式在使用者模式或核心模式下執行。
  • 就緒:程式準備好執行,只需等待核心排程。
  • 睡眠:程式在主記憶體中等待某些事件發生。
  • 建立:程式剛剛被建立,還沒有進入就緒或睡眠狀態。
  • 僵屍:程式已經呼叫了 exit() 系統呼叫,但仍然留下離開碼。

此圖示

  graph TD
    A[Created] --> B[Ready]
    B --> C[Executing]
    C --> D[Sleeping]
    D --> B
    C --> E[Zombie]

解說

此圖示展示了程式在不同狀態之間的轉換。從「建立」狀態開始,程式可以進入「就緒」狀態,然後被排程到「執行」狀態。如果需要等待某些事件,程式會進入「睡眠」狀態,並在事件完成後回到「就緒」狀態。當程式呼叫 exit() 時,會進入「僵屍」狀態。

程式控制塊(PCB)

每個程式在作業系統中都由一個 PCB(Process Control Block)來表示。PCB 包含了以下資訊:

  • 程式狀態:如新建、執行、等待等。
  • 程式計數器:指向下一條要執行的指令地址。
  • CPU 暫存器:包括累加器、索引暫存器、堆積疊指標和一般目的暫存器。
  • CPU 排程資訊:如優先順序、排程佇列指標等。
  • 記憶體管理資訊:如基礎和限制暫存器值、頁面表等。
  • 帳戶資訊:如 CPU 使用時間等。
  • I/O 情況資訊:如分配給該程式的 I/O 裝置列表、開啟的檔案列表等。

建立子程式

在 UNIX 中,程式通常透過 fork() 函式來建立子程式。以下是一個範例:

int main()
{
    int pid;

    pid = fork();
    if (pid < 0)
        printf("Fork failed \n");
    else
    {
        if (pid == 0)
        {
            printf("In child process pid is %d \n", getpid());
            printf("In child process parents pid is %d \n", getppid());
        }
        else
        {
            printf("In parent process pid is %d \n", getpid());
            printf("In parent process parents parent pid is %d \n", getppid());
        }
    }
}

輸出解析

fork() 操作成功,父子程式會分別執行不同的程式碼路徑。子程式的 PID 是父程式傳回的 PID 值為零。

輸出範例:
In parent process pid is xxx (假設為1234)
In parent process parents parent pid is xxx (假設為1234)
In child process pid is xxx (假設為5678)
In child process parents pid is xxx (假設為1234)

問題與解決方案

  1. 資源限制:如果系統中的資源限制(如最大允許的開啟檔案數量)超過限制時,fork() 函式可能會失敗。
  2. 親子過多:如果父子關係錯綜複雜時容易產生僵屍子進行(zombie),導致系統資源浪費。

增強資源管理

可透過對系統進行最佳化來提高效率:

  1. 最佳化檔案系統使用:確保系統可處理大量開啟檔案數量;
  2. 增加監控機制:監控及限制不必要的子進行建立。

程式管理與執行控制

在 Unix-like 系統中,程式管理是軟體開發中的一個重要概念。以下將探討如何使用 wait() 函式來管理子程式的終止狀態,以及如何使用 exec() 函式來執行新的程式。此外,還會介紹如何存取使用者資訊,包括使用者詳細資料、群組詳細資料以及已登入使用者的資訊。

子程式管理與 wait() 函式

當父程式有未終止的子程式時,父程式會暫停直到接收到訊號。這個訊號在子程式終止時即會傳送。wait() 函式可以告訴我們子程式的終止方式。為此,我們需要將一個整數變數傳遞給 wait()。如果子程式正常終止,則傳遞給 wait() 的整數變數的高階 8 個位元會被更新,而低階 8 個位元會被初始化為 0。

相反地,如果子程式異常終止,則低階 8 個位元會被更新,而高階 8 個位元會被初始化為 0。在核心傾印錯誤的情況下,wait() 會傳回一個整數,其中第 7 個位元被設定。

以下是 wait() 函式的範例:

/* wait() 的範例 */
main()
{
    int i, pid, exitstat, status;
    pid = fork();
    if (pid == 0)
    {
        printf("Enter exit stat:");
        scanf("%d", &i);
        exit(i);
    }
    else
    {
        wait(&status);
        if ((status & 0xff) != 0) /* 強制離開 */
        {
            printf("Signal interrupted\n");
        }
        else /* 正常離開 */
        {
            exitstat = (int) status / 256;
            printf("Exit status from %d was %d\n", pid, exitstat);
        }
    }
}

在此範例中,子程式會讓使用者輸入一個離開碼,然後以這個離開碼離開。父程式會在 status 變數中接收到這個離開碼。我們可以檢查 status 變數來瞭解離開的型別。

應用場景

fork() 函式會在子程式中複製所有變數,因此全域變數也會被複製。如果在一個程式中修改值,另一個程式中的值不會反映這些變更。即使指標變數所指向的位置值對不同的程式來說也是不同的。

執行新的程式

現有的程式可以透過 exec 系統呼叫來呼叫另一個程式。執行 exec() 函式後,執行中的程式 ID 不會改變,因為 exec() 函式並不建立新的程式而是呼叫現有的程式。exec() 函式的各種原型如下:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

這些函式的 path 或 file 包含要呼叫的檔案名稱,arg 是傳遞給呼叫檔案的一個字串指標。根據慣例,第一個引數應該指向與要執行的檔案相關聯的檔案名稱。引數列表必須以 NULL 指標結尾。

應用場景

execlp() 和 execvp() 函式會模仿 shell 在搜尋可執行檔案時的行為,如果指定的檔案名稱中不包含斜槓(/)字元。搜尋路徑由環境中的 PATH 變數指定。如果未指定此變數,則使用預設路徑 :/bin:/usr/bin。此外,某些錯誤會被特別處理。

新建立的過程繼承了以下屬性:過程 ID 和父過程 ID、真實使用者 ID 和真實群組 ID、工作階段 ID、當前工作目錄、根目錄、檔案模式建立標記等。

新過程屬性

如果新建立過程有設定 set-user-ID 和 set-group-ID 的位元位置,則呼叫過程有可能改變新過程效能 ID 和群組 ID。

認證取得系統使用者資料

Unix 提供多種函式和系統呼叫以供存取 UNIX 使用者資料。

查詢使用者細節

passwd.h 檔標頭檔案中的 passwd 建構包含類別似 /etc/passwd 資料夾中的使用者資訊:

#include <pwd.h>
struct passwd {
    char *pw_name;     /* using name */
    char *pw_passwd;   /* password */
    uid_t pw_uid;      /* user id*/
    gid_t pw_gid;      /* group id */
    time_t pw_change;  /* validity and password class */
    char *pw_class;    /* full user name */
    char *pw_gecos;
    char *pw_dir;      /* login directory */
    char *pw_shell;    /* user login shell */
    time_t pw_expire;  /* password expiry date*/
};

passwd 建構由 getpwnam() 和 getpwuid() 函式傳回並提供了使用者帳戶資訊。getpwnam() 和 getpwuid() 函式在使用者資料函式庫中搜尋具有比對名稱或 UID 的條目。

getuid() 函式取得目前登入使用者之 UID。

以下是印出使用者詳細資料範例:

#include <pwd.h>

main()
{
    struct passwd *pass;
    int uid;
    uid = getuid();
    pass = getpwuid(uid);
    printf("Login name: %s\n", pass->pw_name);
    printf("Encrypted Password: %s\n", pass->pw_password);
    printf("User ID: %d\n", pass->pw_uid);
    printf("Group ID: %d\n", pass->pw_gid);
    printf("Password Age: %s\n", pass->pw_age);
    printf("Comment: %s\n", pass->pw_comment);
    printf("Login Dir: %s\n", pass->pw_dir);
    printf("Shell: %s\n", pass->pw_shell);
}

查詢群組細節

grp.h 檔標頭檔案中的 group 建構包含群組資訊如 /etc/group 資料夾所顯示:

#include <grp.h>
struct group {
char *gr_name;     /* group name */
char *gr_passwd;   /* group password */
gid_t gr_gid;      /* group id*/
char **gr_mem;     /* pointer to group member names */
};

getgid() 函式取得目前登入使用者之 GID;而 getgrgid() 則用於填充群組結構。

以下範例印出群組詳細資訊:

#include <grp.h>
main()
{
int I;
struct group *grp;
grp = getgrgid(getgid());
printf("Group Name: %s\n", grp->gr_name);
printf("Group Password: %s\n", grp->gr_passwd);
printf("Group ID: %d\n", grp->gr_gid);
printf("Group Members :");
for (I = 0; grp->gr_mem[I]; I++)
printf("\n: %s", grp->gr_mem[I]);
}

查詢所有登入使用者資訊

utmp.h 檔標頭檔案中的 utmp 建構包含類別似 /etc/utmp 的已登入使用者資訊:

struct utmp {
char ut_user[8];          /* User login name */
char ut_id[4];            /* /etc/inittab id (usually the line number) */
char ut_line[12];         /* device name (console, lnxx) */
short ut_pid;             /* process id */
short ut_type;            /* type of entry */
struct exit_status ut_exit;/* The exit status of a process */
time_t ut_time;           /* time entry was made */
};

以下是顯示已登入使用者資訊範例:

#include <sys/types.h>
#include <stdio.h>
#include <utmp.h>
#include <pwd.h>
#define UTMP "/etc/utmp"
#define NAMELEN 8

main()
{
FILE *fp;
struct utmp u;
struct passwd *p;
char temp[NAMELEN + 1];
fp = fopen(UTMP, "r");
if (fp == NULL)
{
perror("open");
exit(EXIT_FAILURE);
}

while(fread(&u, sizeof(u), 1, fp))
{
if(u.ut_type == USER_PROCESS)
{
strncpy(temp,u.ut_user,NNAMELEN)
temp[NNAMELEN]='\0';
printf("%-8.8s %-12.12s %-6.6s","name","line","pid")
printf("%-8.8s %-12.12s %-6d",
u.ut_user,u.ut_line,u.ut_pid)
}
}
fclose(fp)
}

執行解說:

這段 C 語言用於查詢目前所有已登入系統之使用者詳細資料:

  1. 開啟 /etc/utmp:透過 fopen() 張開檔案。
  2. 讀取資料:透過 fread() 每次從 /etc/utmp 中讀取單筆紀錄。
  3. 判斷紀錄型別:ut_type 是 USER_PROCESS 的紀錄即代表已登入之使.
  4. 顯示結果:對於每筆紀錄中 login name、line(tty)、以及 pid(process id)進行顯示。
  5. 結束:完成後關閉檔案即可。

用途:

  • 作業系統管理員可以快速知道有哪些人正在系統上作業。
  • 安全性監控時可以檢視有沒有不正常之動作發生。
  • 若發生異常情形時可以快速回查哪些人可能涉及其中。

注意事項:

  • 對於大型系統而言需要考慮併發存取情形。
  • 需要適當許可權才能存取 /etc/utmp。
  • 在某些特殊環境中可能需要考慮跨平台相容性問題。