Linux基礎第五章 進程控制

5.2 fork

fork函數實現進程複製,相似於動物界的單性繁殖,fork函數直接建立一個子進程。這是Linux建立進程最經常使用的方法。在這一小節中,子進程概念指fork產生的進程,父進程指主動調用fork的進程。程序員

fork後,子進程繼承了父進程不少屬性,包括:shell

  • 文件描述符:至關與dup,標準輸入標準輸出標準錯誤三個文件數組

  • 帳戶/組ID:session

  • 進程組ID多線程

  • 會話ID函數

  • 控制終端this

  • set-user-ID和set-group-ID標記spa

  • 當前工做目錄命令行

  • 根目錄線程

  • umask

  • 信號掩碼

  • 文件描述符的close-on-exec標記

  • 環境變量

  • 共享內存

  • 內存映射

  • 資源限制

可是也有一些不一樣,包括:

  • fork返回值

  • 進程ID

  • 父進程

  • 進程運行時間記錄,在子進程中被清0

  • 文件鎖沒有繼承

  • 鬧鐘

  • 信號集合

    #include <stdio.h>
    #include <unistd.h>
     #include <sys/types.h>
           #include <sys/stat.h>
           #include <fcntl.h>
    
    int main()
    {
        printf("before fork\n");
    
        // 在父進程中打開的文件描述符
    //    int fd = open("a.txt", O_RDWR|O_CREAT, 0777);
    
    //    FILE* fp = fopen("a.txt", "r");
        int fd = open("a.txt", O_RDWR);
        pid_t pid = fork(); // 建立一個新進程
        if(pid == 0)
        {
            // 子進程可使用父進程的描述符
      //      write(fd, "hello", 5);
        
          //  char ch = fgetc(fp);
            char ch;
            read(fd, &ch, 1);
            printf("ch is %c\n", ch);
    
            printf("this is in child, ppid=%d\n", (int)getppid());
        }
        else if(pid > 0)
        {
        //    write(fd, "world", 5);
            char ch;
            read(fd, &ch, 1);
            printf("ch is %c\n", ch);
    
    
            // 當fork返回值大於0時,說明該進程是父進程
            // 此時,返回值就是子進程的pid
            printf("this is in parent, pid=%d\n", (int)getpid());
        }
        else
        {
            printf("error fork\n");
        }
    
        printf("hello fork\n");
    }
    #include <stdio.h>
    #include <unistd.h>
    
    int global_var = 0;//fork()出來的子進程的值改變,不會影響父進程  由於開開闢了新的空間
    
    int main()
    {
        int var = 0;
        int* p = (int*)malloc(sizeof(int));
        *p = 0;
    
        pid_t pid = fork();
        if(pid == 0)
        {
            global_var = 100;
            *p = 100;
            var = 100;
            printf("set var\n");
        }
        else if(pid > 0)
        {
            sleep(1);
            // 肯定的結果,就是0
            printf("%d\n", global_var);
            printf("var is %d\n", var); // 0
            printf("*p = %d\n", *p);
        }
        
        printf("hello world\n");
    }
    #include <stdio.h>
    #include <unistd.h>
    
    void forkn(int n)
    {
        int i;
        for(i=0; i<n; ++i)
        {
            pid_t pid = fork();
            if(pid == 0)
                break;
        }
    }
    
    int main()
    {
        forkn(10);
    
        printf("hello world\n");
    }

     

5.3 進程終止

進程有許多終止方法:

方法  
main函數return 正常退出
調用exit或者_Exit或者_exit 正常退出
在多線程程序中,最後一個線程例程結束 正常退出
在多線程程序中,最後一個線程調用pthread_exit 正常退出
調用abort 異常退出
收到信號退出 異常退出
多線程程序中,最後一個線程響應pthread_cancel 異常退出

當進程退出時,內核會爲進程清除它申請的內存,這裏的內存是指物理內存,好比棧空間、堆、代碼段、數據段等,而且關閉全部文件描述符。

通常來講,進程退出時,須要告訴父親進程退出的結果,若是是正常退出,那麼這個結果保存在內核的PCB中。若是是異常退出,那麼PCB中保存退出結果的字段,是一個不肯定的值。所以程序員應該避免程序的異常退出。

進程退出時,除了它的PCB所佔內存,其餘資源都會清除。

5.4 wait和waitpid

一個進程終止後,其實這個進程的痕跡尚未徹底被清除,由於還有一個PCB在內核中,若是不回收,那麼會致使內存泄漏。父進程能夠調用wait函數來回收子進程PCB,並獲得子進程的結果。

wait是一個阻塞調用,它的條件是一個子進程退出或者一個子進程有狀態變化。
wait獲得的status,包含了子進程的狀態變化緣由和退出碼信息等等。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
       #include <sys/wait.h>


int main()
{
    pid_t pid = fork();

    if(pid == 0)
    {
        sleep(1);
        printf("child process\n");

        return 18;
    }
    else if(pid > 0)
    {
        printf("parent process\n");

        // 等待子進程結束,而且回收子進程的PCB
        int status;
        wait(&status);

        // 如何獲得子進程的返回值
        if(WIFEXITED(status))
        {
            printf("normal child process exit\n"); // 正常退出

            int code =WEXITSTATUS(status);
            printf("code is %d\n", code);
        }
        else if(WIFSIGNALED(status))
        {
            printf("signal\n");
        }
        else if(WIFSTOPPED(status))
        {
            printf("child stopped\n");
        }
        else if(WIFCONTINUED(status))
        {
            printf("child continue...\n");
        }


        printf("after wait\n");
    }



    return 0;
}

 

wait和waitpid可能會阻塞父進程,因此通常使用SIGCHLD信號來監控子進程

5.5 殭屍進程和孤兒進程

5.5.1 殭屍進程

是指已經退出的進程,可是父進程沒有調用wait回收的子進程。殭屍進程沒有任何做用,惟一的反作用就是內存泄漏。若是父進程退出,那麼它的全部殭屍兒子會獲得清理,所以殭屍進程通常指那些用不停歇的後臺服務進程的殭屍兒子。

程序員應該避免殭屍進程的產生。

#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t pid = fork();
    if(pid == 0)
    {
        // 子進程什麼事兒都不幹,退出了,此時子進程是殭屍進程
    }
    else if(pid > 0)
    {
        getchar(); // 父進程不退出
    }

    return 0;
}

 

5.5.2 孤兒進程

父進程退出了,而子進程沒有退出,那麼子進程就成了沒有父親的孤兒進程。孤兒進程不會在系統中出現很長時間,由於系統一旦發現孤兒進程,就會將其父進程設置爲init進程。那麼未來該進程的回收,由init來負責。

5.6 exec

exec函數執行一個進程,當一個進程調用exec後,調用該函數的進程的虛擬地址空間的代碼段、數據段、堆、棧被釋放,替換成新進程的代碼段、數據段、堆、棧,而PCB依舊使用以前進程的PCB。這個函數用中文來講就是鳩佔鵲巢。

exec後使用的是同一個PCB,因此exec以後和以前,由不少進程屬性是相同的,包括:

  • 進程ID和父進程ID

  • 帳戶相關

  • 進程組相關

  • 定時器

  • 當前目錄和根目錄

  • umask

  • 文件鎖

  • 信號mask

  • 未決的信號

  • 資源限制

  • 進程優先級

  • 進程時間

  • 沒有close-on-exec屬性的文件描述符

使用fork和exec來執行一個新程序

#include <unistd.h>
#include <stdio.h>

// execle, e表示環境變量environ
//
int main(int argc, char* argv[])
{
    char* args[] = {
        "/bin/ls",
        "-a",
        "-l",
        NULL
    };
    execv("/bin/ls", args);
}

int main2(int argc, char* argv[])
{
    // p表示在PATH的環境變量中尋找這個程序
    execlp("ls", "ls", NULL);
}

int main1(int argc, char* argv[])
{
    // 執行一個程序
    execl("/bin/ls", "/bin/ls", "-a", "-l", NULL);

    // 該函數不會被執行
    printf("hello world\n");
}
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    // fd is 3
    int fd = open("exec.txt", O_RDWR|O_CREAT|O_CLOEXEC, 0777);

    execl("./exec_test", "./exec_test", NULL);
}

 

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[]);
int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
 
函數後綴 解析
l list 用不定參數列表來表示命令參數,若是用不定參數列表,那麼用NULL表示結束
v vector 用數組來傳遞命令行參數
p path 表示程序使用程序名便可,在$PATH中搜索該程序,不帶p的須要提供全路徑
e environ 表示環境變量

補充:不定參數

不定參數函數定義:

#include "../h.h"
 
#define mylog(fmt, ...) myprint(__FILE__, __LINE__, fmt, __VA_ARGS__)
 
void myprint(const char* filename, int line, const char* fmt, ...)
{
    printf("%s, %d, ", filename, line);
    va_list ap;
    va_start(ap, fmt);
    vprintf(fmt, ap);
    va_end(ap);
}
 
int print(const char* a, ...)
{
    const char* arg = a;
    va_list args;
    va_start(args, a);
 
    while(arg)
    {
        printf("%s\n", arg);
        arg = va_arg(args, const char*);
    }
#if 0
    printf("%s\n", a);
 
    while(1)
    {
        const char* arg = va_arg(args, const char*);
        if(arg == NULL)
            break;
        printf("%s\n", arg);
    }
#endif
 
    va_end(args);
}
 
int add(int count, ...)
{
    int i;
    int sum = 0;
    va_list args;
    // 得到不定參數的首地址
    va_start(args, count);
 
    for(i=0; i<count; ++i)
    {
        // 經過va_arg得到參數
        int arg = va_arg(args, int);
        sum += arg;
    }
    // 參數獲取完畢
    va_end(args);
 
    return sum;
}
 
int main()
{
    myprint(__FILE__, __LINE__, "haha%d\n", 100);
    mylog("print in mylog %d\n", 100);
    print("hello", "world", "haha", "you are dead", NULL);
    int ret = add(3, 5, 6, 7);
    printf("%d\n", ret);
    return 0;
}
int main()
{
    int a = add(3, 12, 13, 14);
    int b = add(2, 12, 13);
    int c = add(4, 12, 13, 14, 15);
     printf("%d, %d, %d\n", a, b, c);

    char* p = concat("abc", "bcd", NULL);
    printf("p is %s\n", p);

    // 最後的NULL,被稱之爲哨兵
    p = concat("aaaa", "bbbb", "cccc", NULL);
    printf("p is %s\n", p);

}
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>

// 若是沒有__VA_ARGS__不帶##,表示__VA_ARGS__至少要表示一個參數
// #define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, __VA_ARGS__)

// __VA_ARGS__若是有##,表示能夠沒有參數
#define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, ##__VA_ARGS__)

int main()
{
    int fd = open("a.txt", O_RDWR);
    if(fd < 0)
    {
        mylog("error open file\n");
    }
}
#include <stdio.h>

// 轉字符串 abc "abc"
#define STR(a) #a

// 拼接標識符
#define CC(a, b) a##b

int main()
{
    int abcxyz = 100;
    printf("%d\n", CC(abc, xyz));
}

 

5.8 帳戶和組控制

Snip20161008_26
Snip20161008_28

5.9 進程間關係

在Linux系統中,進程間除了有父子關係,還有組關係、Session關係、進程和終端進程關係。設計這些關係是爲了更好的管理進程。

5.9.1 Session

一次登錄算一個session,exit命令能夠退出session,session包括多個進程組,一旦session領導退出,那麼一個session內全部進程退出(它的全部進程收到一個信號)。

#include <unistd.h>
int main()
{
    pid_t pid = fork();

    if(pid == 0)
    {
            // 獨立一個session
            setsid();
    }
    
    while(1)
    {
        sleep(1);
    }
}

 

5.9.2 進程組

在終端執行進程,就會生成一個進程組。執行的進程fork以後,子進程和父進程在一個組中。

進程組長退出後,進程組的其餘進程的組號依舊沒有變化。

5.10 練習

5.10.1 fork任意個子進程。

int main()
{
    int i;
    for(i=0; i<7; ++i)
    {
        pid_t pid = fork();
        if(pid == 0)
            break;
    }
}

5.10.2 使用多進程加速文件拷貝

./mycp -job 4 srcfile destfile

使用-job定義進程數量,加速文件拷貝。

 
#include "../h.h"
 
int get_file_length(const char* filename)
{
    struct stat buf;
    int ret = stat(filename, &buf);
    if(ret == -1)
        return -1;
    return buf.st_size;
}
 
void process_copy(const char* src_file, const char* dst_file, int pos, int length)
{
    FILE* src = fopen(src_file, "r");
    FILE* dst = fopen(dst_file, "r+");
 
    char buf[4096];
    int block_size = sizeof(buf);
 
    fseek(src, pos, SEEK_SET);
    fseek(dst, pos, SEEK_SET);
 
    while(length)
    {
        int copy_len = length < block_size ? length : block_size;
        int ret = fread(buf, 1, copy_len, src);
        fwrite(buf, ret, 1, dst);
        length -= ret;
    }
 
    fclose(src);
    fclose(dst);
}
 
// ./multi-process-cp -job n srcfile dstfile
int main(int argc, char* argv[])
{
    if(argc != 5)
    {
        printf("usage %s -job {process_count} {src_file} {dst_file}\n", argv[0]);
        return 1;
    }
 
    if(strcmp(argv[1], "-job") != 0)
    {
        printf("unknown options: %s\n", argv[1]);
        return 2;
    }
 
    int process_count = atoi(argv[2]);
    if(process_count <= 0)
    {
        printf("process count error\n");
        return 3;
    }
 
    const char* src_file = argv[3];
    const char* dst_file = argv[4];
 
    // 得到文件總長度
    int filelen = get_file_length(src_file);
    if(filelen == -1)
    {
        printf("file not exist\n");
        return 3;
    }
 
    // 保證dst文件存在,而且dst的文件尺寸是src文件同樣大
    int fd = open(dst_file, O_CREAT|O_WRONLY, 0777);
//    ftruncate(fd, filelen);
    close(fd);
    truncate(dst_file, filelen);
 
    // 4 process 21 字節 21/4 = 5
    // 0 0~4
    // 1 5-9
    // 2 10-14
    // 3 21-15 6
    int i;
    int average = filelen / process_count;
    // 只要建立n-1個子進程,父進程負責最後部分的拷貝
    for(i=0; i<process_count-1; ++i)
    {
        pid_t pid = fork();
        if(pid == 0)
        {
            // 子進程拷貝完成直接結束
            int pos = average * i;
            process_copy(src_file, dst_file, pos, average);
            return 0;
        }
    }
 
    int pos = average * i;
    process_copy(src_file, dst_file, pos, filelen - pos);
 
    // wait一次只wait一個子進程
    for(i=0; i<process_count-1; ++i)
        wait(NULL);
 
    return 0;
}

5.10.3 實現自定義終端

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

// ls
// mkdir aaa
// cp ../aa bb
// cd
void handle_cmd(char* cmd)
{
    char* args[1024];
    char* p = strtok(cmd, " ");
    int i = 0;
    while(p)
    {
        args[i++] = p;
        p = strtok(NULL, " ");
    }
    args[i] = NULL; // 表示參數結束位置

    if(strcmp(args[0], "cd") == 0)
    {
        // 切換當前目錄
        chdir(args[1]);
        return;
    }


    pid_t pid = fork();
    if(pid == 0)
    {
        execvp(args[0], args);
        // 若是命令執行失敗,應該讓子進程退出
        printf("invalid command\n");
        exit(0);
    }
    else
    {
        wait(NULL); 
    }
}

int main()
{
    while(1)
    {
        printf("myshell> ");
        // 等待用戶輸入
        char buf[4096];
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf)-1] = 0; // remove \n

        if(strlen(buf) == 0)
        {
            continue;
        }

        handle_cmd(buf);
    }
}

 

5.11 函數和命令

5.11.1 函數

fork:建立子進程exec:執行新的程序wait/waitpid:等待子進程結束,回收子進程PCB內存。va_list:va_start:定義指向不定參數的第一個參數的地址va_arg:從參數列表中獲取一個參數,而且讓指針指向下一個參數va_end:清除ap

相關文章
相關標籤/搜索