1. 程式人生 > >Linux程式設計之程序(二)

Linux程式設計之程序(二)

在上一篇關於程序的部落格中闡述了程序的概念,總結了程序的狀態以及程序如何描述,這篇部落格就總結下程序的建立,等待,終止以及程序等待的作用。

程序的建立

上一篇部落格中講了關於叉函式的使用,其實叉函式就是程序建立的一個重要手段,演示下面的程序建立³³
直接上程式碼

// create.c
#include <stdio.h>
#include <unistd.h>

int main()
{
    //pid_t fork(void);
    //建立一個新程序,通過複製呼叫程序
    pid_t pid = fork();
    if (pid < 0) {
        printf("fork error\n");
        return -1;
    }else if (pid == 0) {
        printf("this is chilld %d!!\n", getpid());
    }else {
        printf("this is parent %d----child:%d\n", getpid(), pid);
    }
    printf("pid:%d\n", pid);
    while(1) {
        sleep(1);
    }
    return 0;
}

程序的等待

問:什麼啥程序的等待?程序為什麼要等待?程序等待的方式?
答:(1)一個程序退出之後因為要儲存自己退出的原因,因此不會釋放所有的資源,它等待父程序檢視它的退出原因,然後釋放所有資源。假如父程序根本不管,那麼這個子程序就成了殭屍程序,造成資源洩漏
(2)為了防止殭屍程序的出現,父程序應該等待子程序退出
(3)第一種方式:wait函式,目的是等待任意一個子程序的退出,因此wait是一個阻塞型函式,如果沒有子程序退出,將一直等待下去,直到子程序退出
第二種方式:waitpid它是一個阻塞/非阻塞可選的函式
函式原型:waitpid(pid_t pid,int * status,int options);

  • pid:-1:等待任意子程序> 0等待指定的子程序
  • status:獲取退出狀態碼
  • 選項:0:阻塞WNOHANG:非阻塞
  • 返回值:-1:出錯== 0:沒有子程序退出> 0:退出的子程序pid下面演示程序等待
//wait.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
    pid_t pid = fork();
    if (pid < 0) {
        exit(-1);
    }else if (pid == 0) {
        sleep(3);
        exit(99);
    }
    pid_t id = -1;
    /*
    if ((id = wait(NULL)) < 0) {
        perror("wait error");
    }
    */
    //第二種方式
    int status = -1;
    while((id = waitpid(pid, &status, WNOHANG)) == 0) {
        //回過頭再判斷一下有沒有子程序退出
    }
    /*
    if ((status & 0x7f) == 0) {
        printf("child exit status:[%d]\n", (status >> 8)&0xff);
    }
    */
    if (WIFEXITED(status)) {
        printf("child exit status:[%d]\n", WEXITSTATUS(status));
    }
    while(1) {
        sleep(1);
    }
    printf("child :%d eixt %d\n", id, pid);
    
    return 0;
}

程序的終止

程序退出的場景:

  • 執行完畢,結果正確
  • 執行完畢,結果不正確
  • 程式碼異常終止
    常見退出方法
    正常退出:1)main函式中返回
    2)退出是溫和退出,退出前溫和的釋放資源,重新整理緩衝區
    3)_exit是暴力退出,直接釋放資源,不會重新整理緩衝區
    上面的程式碼中我們看到exit這樣的函式,接下來就說說exit函式和_exit
    _exit函式:void _exit(int status); 引數:status定義了程序的終止狀態,父程序通過wait來獲取該值
    exit函式:void exit(int status);該函式在呼叫exit之前還做了一些事:1。執行使用者通過atexit或on_exit定義的清理函式2.關閉所有開啟的流,所有的快取資料均被寫入3.呼叫_exit總結
    來說:退出釋放資源退出,_exit立即退出。

還有一種我們常用的return n退出,它就和退(n)等同,因為呼叫main函式的執行時函式會將main的返回值當做exit的引數

程序程式替換

程式替換的是程式碼段所指向的實體記憶體區域,相當於讓虛擬地址空間中的程式碼地址指向了實體記憶體的另一端程式碼位置,這樣的話虛擬地址空間中原先的資料區域以及堆疊都會重新初始化,因為現在的程式碼執行的根本不是複製的那些資料但是這個程序pcb還是原來的pcb
execl函式族:

C p Ë
EXECL execlp execle
execv execvp 的execve

升和v的區別:L是引數平鋪一個一個通過EXEC函式引數賦予,V引數直接使用字串指標陣列

execl / execv需要我們給出要替換的程式的全路徑名
execlp / execvp只需要給出替換的程式的名稱就行
execle重新自己組織環境變數,不使用現有的
下面演示一個exec函式的使用

//exec.c
#include <errno.h>

int main()
{
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork error");
        exit(-1);
    }else if (pid == 0) {
        printf("-----------\n");
        //int execl(const char *path, const char *arg, ...);
        //execl("/bin/ls", "ls", "-l", "-a", NULL);
        //int execlp(const char *file, const char *arg, ...);
        //execlp("/home/san/workspace/36/pctrl/test" "test", NULL);
        //int execle(const char *path, const char *arg, ..., 
        //  char * const envp[]);
        //  新增引數的時候記住要有一個NULL表示引數的結尾
        //  NULL之後還有一個引數是用於設定環境變數的
        //      並且這個函式會清空所有的環境變數,因為這個介面就是
        //      讓我們使用者自己來設定環境變數的
        char *ptr = "PATH=hehe---he----hehe!!";
        char *env[3] = {NULL};
        env[0] = ptr;
        execle("/code/day10_30/test", "test", NULL, 
                env);
        perror("execle error");

        //這句程式碼實際上是根本不會執行的,因為程式碼段已經被替換了
        printf("-----------\n");
    }
    printf("hehe!!!");
    return 0;
}

如果想了解EXEC函式原型可以自行在Linux的下男人

Myshell實現

學了程序的建立,等待,終止,我們可以自行做一個簡易的shell工具,它的功能就是解釋我們輸入的命令,初步看起來像shell
實現步驟:
1.鍵盤接收輸入資訊
2.建立子程序
3.程式替換

//myshell.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    while(1) {
        printf("minishell: ");
        fflush(stdout);
        char cmd[1024] = {0};
        
        if (scanf("%[^\n]%*c", cmd) != 1) {
            getchar();
        }
        //將獲取到的命令解析一下,然後建立子程序進行程式替換
        char *ptr = cmd;
        char *argv[32] = {NULL};
        int argc = 0;
        argv[argc++] = ptr;
        while(*ptr != '\0') {
            //ls       -l
            //int isspace(int c);
            //用於判斷一個字元是否是:\t \n \r 空格 
            //解析一個字串時候這裡就是對空格的判斷
            if (isspace(*ptr)) {
                while(isspace(*ptr) && *ptr != '\0') {
                    *ptr++ = '\0';
                }
                argv[argc++] = ptr;
            }
            ptr++;
        }
        if  (fork() == 0) {
            execvp(argv[0], argv);
        }
        //需要等待的原因:
        //1. 避免產生殭屍子程序
        //2. 是為了等待子程序執行完畢,讓程式邏輯更加完善
        wait(NULL);
        
    }
    return 0;
}

上面的程式中可能你會對下面這個解釋有疑問,因此在此做出解釋(正則表示式):

  • ^ \ n:scanf本身是遇到空格就要獲取一次,這樣的話就無法獲取到一個完整的命令,因此'%[^ \ n]'表示的是獲取資料直到遇到\ n為止
  • %* c:將緩衝區中的字元都取出來,但是不要它,直接丟棄目的是為了將最後的\ n從緩衝區取出來,防止陷入死迴圈