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是要映射字节在文件中的起始偏移量;另外,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/