C語言的clone與mmap調用


 

clone

linux 創建線程(pthread_create)和進程(fork)的過程非常類似,都是主要依賴 clone 函數,只不過傳入的參數不同而已。

如此一來,內核只需要實現一個 clone函數,就既能創建進程,又能創建線程了,例如;

創建進程:

clone(SIGCHLD)

 

創建線程:

clone(CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD)

其實,linux 內核沒有嚴格區分線程和進程,也沒有准備特別的調度算法或是定義特別的數據結構來描述線程。相反,線程僅僅被視為一個與其他進程共享某些資源的進程,在 linux 中,線程看起來就像是一個輕量級的進程(light weight process)。

 

看下clone函數原型:

/* Prototype for the glibc wrapper function */

#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
        /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

/* Prototype for the raw system call */

long clone(unsigned long flags, void *child_stack,
           void *ptid, void *ctid,
           struct pt_regs *regs);

glibc clone函數是對clone系統調用的一個封裝。

能夠看出,clone 函數是一個不定參數的函數。它主要用於創建新的進程(也包括線程,因為線程是“特殊”的進程),調用成功后,返回子進程的 tid,如果失敗,則返回 -1,並將錯誤碼設置再 errno。

clone 函數的第1個參數fn是一個函數指針;第2個參數child_stack是用於創建子進程的棧(注意需要將棧的高地址傳入);第3個參數flags,就是用於指定行為的參數了。

flags參數列舉如下:

  • CLONE_FILES,父子進程共享打開的文件
  • CLONE_FS,父子進程共享文件系統信息
  • CLONE_VM,父子進程共享地址空間
  • CLONE_SIGHAND,父子進程共享信號處理函數,以及阻斷的信號
  • CLONE_THREAD,父子進程放入相同的線程組

 

看個用clone() 創建線程的例子

//#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/syscall.h>

#define STACK_SIZE 8192*1024
#define     gettid()                    ((int)syscall(SYS_gettid))

void* testOcupMem(void* p) {
    printf("    %d-%d testOcupMem exit\n", getpid(), gettid());
    usleep(100);
    return NULL;
}

int main() {
    void* pstk = malloc(STACK_SIZE);
    if (NULL == pstk){
         printf("cannot malloc stack\n");
         return -1;
    }

        printf("main: %d-%d\n", getpid(), gettid());
    for (int i = 0; i < 5; i++) {

        int clonePid = clone((int(*)(void*))testOcupMem,  (char *)pstk + STACK_SIZE,
                     CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD, NULL);
        if (-1 ==  clonePid) {
            printf("query failed, cannot start query process\n");
            free(pstk);
            return -1;
        }


        struct timeval val;
        gettimeofday(&val, 0);
        printf("%d:%ld clonePid: %d-%d\n\n", i, val.tv_sec * 1000 + val.tv_usec / 1000, getpid(), clonePid);

        usleep(1000 * 1000);
    }

    free(pstk);
    printf("\n------------- getchar --------------\n");
    getchar();
    return 0;
}

說明:

Linux中,每個進程有一個pid,類型pid_t,由getpid()取得。Linux下的POSIX線程也有一個id,類型 pthread_t,由pthread_self()取得,該id由線程庫維護,其id空間是各個進程獨立的(即 不同進程中的線程可能有相同的id)。Linux中的POSIX線程庫實現的線程其實也是一個進程(LWP),只是該進程與主進程(啟動線程的進程)共享一些資源而已,比如代碼段,數據段等。
有時候我們可能需要知道線程的真實pid。比如進程P1要向另外一個進程P2中的某個線程發送信號時,既不能使用P2的pid,更不能使用線程的pthread id,而只能使用該線程的真實pid,稱為tid。有一個函數gettid()可以得到tid,但glibc並沒有實現該函數,只能通過Linux的系統調用syscall來獲取。

 

輸出結果:

main: 28942-28942
0:1606723498039 clonePid: 28942-28943

28943-28943 testOcupMem exit
1:1606723499039 clonePid: 28942-28945

1:1606723499039 clonePid: 28942-28945

28945-28945 testOcupMem exit
2:1606723500039 clonePid: 28942-28986

2:1606723500039 clonePid: 28942-28986

28986-28986 testOcupMem exit
3:1606723501039 clonePid: 28942-28989

3:1606723501039 clonePid: 28942-28989

28989-28989 testOcupMem exit
4:1606723502039 clonePid: 28942-29008

4:1606723502039 clonePid: 28942-29008

29008-29008 testOcupMem exit

從輸出內容可見:

1、在testOcupMem函數打印的getpid都是同一個值,說明每次clone創建的線程屬於同一個父進程(main進程),但 tid 各不相同;

2、為什么main函數for循環的 clonePid 日志會重復2次呢(看時間戳應該是同一個時間點打印的)?據我感覺應該是clone調用會在一個新線程開始執行testOcupMem函數,而這個函數如果后於printf執行,可能創建的新線程也執行了printf,這樣當main線程打印的時候就有2條日志。當然,這個只是我的猜測,還有待驗證。

 

PS:如果想用clone創建進程,在上面的例子中,把clone函數的flags參數去掉 CLONE_VM | CLONE_FS  | CLONE_FILES 即可。

 

 

 

mmap

mmap函數把一個文件或一個POSIX共享內存區對象映射到調用進程的地址空間,使用該函數有3個目的:

1、使用普通文件以提供內存映射I/O

2、使用特殊文件以提供匿名內存映射;

3、使用shm_open以提供無親緣關系進程間的POSIX共享內存區。

 

存儲映射I/O使一個磁盤文件的全部或部分內容映射到用戶空間中,將進程讀寫文件的操作變成了讀寫內存的操作(不再需要read/write調用)。

 

API:

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off);
int mprotect(void *addr, size_t len, int prot);
int msync(void *addr, size_t len, int flags);
int munmap(void *addr, size_t len);

說明:

  • mmap函數中,參數addr用於指定映射存儲區的起始地址,通常將其設置為0,這表示由系統選擇該映射區的起始地址;參數fd指定要被映射文件的描述符,len是映射字節數,off是要映射字節在文件中的起始偏移量;另外,offaddr的值通常應當是系統虛存頁長度的倍數。mmap調用成功時返回該映射區的起始地址。
  • mprotect函數可以更改一個現存映射存儲區的權限;
  • msync函數的作用是把共享存儲映射區中被修改的頁沖洗到被映射的文件中。如果映射是私有的(MAP_PRIVATE),不修改被映射的文件;
  • munmap函數的作用是解除存儲映射區,關閉描述符並不解除映射區。

 

mmap函數中的prot參數

PROT_READ

映射區可讀

PROT_WRITE

映射區可寫

PROT_EXEC

映射區可執行

PROT_NONE

映射區不可訪問

 

mmap函數中的flags參數

MAP_PRIVATE

私有,對映射區的寫操作會導致創建映射文件的一個私有副本

MAP_SHARED

對映射區的寫操作直接修改原始文件,多個進程對同一個文件的映射是共享的,一個進程對映射的內存做了修改,另一個進程也會看到這種變化

MAP_FIXED

返回值必須等於addr,不利於移植性

MAP_ANONYMOUS

匿名映射,此時忽略fd,且映射區域無法與其它進程共享,這個選項一般用來擴展heap

MAP_DENYWRITE

 

MAP_LOCKED

 

 

msync函數中的flags參數,

MS_ASYNC

異步寫

MS_SYNC

同步寫

MS_INVALIDATE

從文件中讀回數據

 

注意:調用fork函數,子進程繼承存儲映射區(子進程復制父進程的地址空間,而存儲映射區是該地址空間的一部分),但調用exec之后的程序則不繼承此存儲映射區。

 

MAP_ANONYMOUS 用法

如下,fd=0,沒有關聯任何文件;

void annoymap(int N) {
    int *ptr = (int *) mmap(NULL, N * sizeof(int),
                            PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS,
                            0, 0);
    if (ptr == MAP_FAILED) {
        printf("Mapping Failed\n");
        return;
    }

    // Fill the elements of the array
    for (int i = 0; i < N; i++) {
        ptr[i] = i;
    }

    // print
    for (int i = 0; i < N; i++) {
        printf("[%d] ", ptr[i]);
    }
    printf("\n");

    if (0 != munmap(ptr, 10 * sizeof(int))) {
        printf("UnMapping Failed\n");
        return;
    }
}

 

 

文件IO映射

如下,以MAP_SHARED 映射指定文件,修改后可以同步到文件中;

void filemap(const char* path) {
    int fd;
    void *start;
    struct stat sb;
    fd = open(path, O_RDWR);

    fstat(fd, &sb);
    start = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (start == MAP_FAILED) {
        printf("mmap failed\n");
        return ;
    }

    close(fd);

    printf("%s", start);
    strncpy((char*)start, "say hi", 6);

    munmap(start, sb.st_size);
}

 

 

共享內存映射

多進程之間共享

void sharememory(int size) {
    char parent_message[] = "hello";
    void* shmem = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    memcpy(shmem, parent_message, sizeof(parent_message));

    if (fork() == 0) {
        printf("Child read: %s\n", shmem);
        char child_message[] = "goodbye";
        memcpy(shmem, child_message, sizeof(child_message));
        printf("Child wrote: %s\n", shmem);

    } else {
        printf("Parent read: %s\n", shmem);
        sleep(1);
        printf("After 1s, parent read: %s\n", shmem);
    }
}

 

 

 

 

 

 

參考:

https://eli.thegreenplace.net/2018/launching-linux-threads-and-processes-with-clone/

https://blog.popkx.com/c-language-actual-warfare-29-how-does-the-linux-kernel-create-processes-and-threads/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM