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; }
說明:
輸出結果:
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是要映射字節在文件中的起始偏移量;另外,off和addr的值通常應當是系統虛存頁長度的倍數。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/
