Linux线程与进程的栈管理、页表机制及线程封装
目录
一、线程栈管理
进程(主线程)栈管理
子线程栈管理
二、页表和页表项
页表标志位定义
关键数据结构
页表分配函数
三、通用线程封装实现
实现要点
四、直接调用clone系统调用
关键点说明
一、线程栈管理
虽然Linux内核将线程和进程统一在task_struct结构中管理,但在地址空间的栈(stack)管理上仍有显著区别:
进程(主线程)栈管理
-
主线程栈:可以简单理解为
main()函数的栈空间 -
创建机制:在
fork()时复制父进程的stack空间地址,采用写时拷贝(COW)机制 -
动态增长:栈空间可以动态增长,但有上限限制
-
溢出处理:
-
超出扩充上限时会触发栈溢出,内核会发送段错误信号(SIGSEGV)给进程
-
进程栈是唯一可以访问未映射页而不一定会立即发生段错误的情况——只有超出扩充上限才会报错
-
子线程栈管理
-
创建方式:通常通过glibc/uclibc的
pthread_create()接口创建 -
内存分配:
-
使用
mmap系统调用在文件映射区(共享区)分配固定大小的栈空间 -
关键代码(来自glibc的
nptl/allocatestack.c):mem = mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); -
默认栈大小通常为8MB(可通过
pthread_attr_setstacksize()调整)
-
-
与进程栈的区别:
-
栈空间是预先固定分配的,不能动态增长
-
用尽栈空间会导致未定义行为(通常是崩溃),而不会像进程栈那样触发动态增长
-
-
系统调用流程:glibc通过
mmap获取栈内存后,调用sys_clone:int sys_clone(struct pt_regs *regs) {unsigned long clone_flags;unsigned long newsp;// ...clone_flags = regs->bx;newsp = regs->cx; // 获取mmap得到的线程栈指针// ...return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr); } -
内存访问特性:
-
线程栈在进程地址空间中通过mmap映射的私有内存区域
-
理论上线程私有,但同一进程的线程会浅拷贝生成者的
task_struct字段,其他线程可能访问到
-
二、页表和页表项
Linux内核采用两级页表结构,维护硬件页表和Linux内部页表两套体系:
页表标志位定义
/* 页表标志位 */
#define L_PTE_PRESENT (1 << 0) // 页表项存在标志
#define L_PTE_FILE (1 << 1) // 仅当!PRESENT时使用,表示非内存映射文件
#define L_PTE_YOUNG (1 << 1) // 页被访问过(用于页面置换算法)
#define L_PTE_BUFFERABLE (1 << 2) // 可缓冲
#define L_PTE_CACHEABLE (1 << 3) // 可缓存
#define L_PTE_USER (1 << 4) // 用户可访问
#define L_PTE_WRITE (1 << 5) // 可写
#define L_PTE_EXEC (1 << 6) // 可执行
#define L_PTE_DIRTY (1 << 7) // 页被修改过
#define L_PTE_COHERENT (1 << 9) // I/O一致性(xsc3)
#define L_PTE_SHARED (1 << 10) // CPU间共享(v6)
#define L_PTE_ASID (1 << 11) // 非全局(使用ASID, v6)
关键数据结构
typedef struct { unsigned long pte; } pte_t; // 页表项
typedef struct { unsigned long pgd; } pgd_t; // 页全局目录项struct mm_struct {struct vm_area_struct *mmap; // 虚拟内存区域链表struct rb_root mm_rb; // VMA红黑树根节点unsigned long mmap_base; // mmap区域基址unsigned long task_size; // 任务虚拟内存空间大小pgd_t *pgd; // 页目录起始地址// ...
};
页表分配函数
页全局目录分配:
pgd_t *pgd_alloc(struct mm_struct *mm) {pgd_t *ret = (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO);// 初始化页表项...return ret;
}
页表项分配:
pte_t *pte_alloc_one_kernel(struct mm_struct *mm, unsigned long address) {pte_t *pte = (pte_t *)__get_free_page(GFP_KERNEL|__GFP_REPEAT|__GFP_ZERO);return pte;
}
三、通用线程封装实现
下面是一个支持任意参数传递的线程封装类实现:
#include <iostream>
#include <functional>
#include <memory>
#include <pthread.h>
#include <unistd.h>class Thread {
public:Thread() : thread_id_(0), running_(false) {}~Thread() {if (running_) {pthread_detach(thread_id_);}}template <typename Callable, typename... Args>bool start(Callable&& func, Args&&... args) {if (running_) {std::cerr << "Thread is already running!" << std::endl;return false;}// 使用完美转发和std::bind打包任务auto task = std::make_shared<std::function<void()>>(std::bind(std::forward<Callable>(func), std::forward<Args>(args)...));// 创建线程,传递任务指针auto* task_ptr = new std::shared_ptr<std::function<void()>>(task);if (pthread_create(&thread_id_, nullptr, &Thread::threadEntry, task_ptr) != 0) {delete task_ptr; // 失败时清理std::cerr << "Failed to create thread!" << std::endl;return false;}running_ = true;return true;}void join() {if (running_) {pthread_join(thread_id_, nullptr);running_ = false;}}private:pthread_t thread_id_;bool running_;static void* threadEntry(void* arg) {// 使用unique_ptr自动管理资源std::unique_ptr<std::shared_ptr<std::function<void()>>> task_ptr(static_cast<std::shared_ptr<std::function<void()>>*>(arg));// 执行任务(*(*task_ptr))();return nullptr;}
};// 示例函数
void printMessage(const std::string& message, int value, int a, int b, int c) {std::cout << "Message: " << message << ", Value: " << value << std::endl;std::cout << "a:" << a << std::endl;std::cout << "b:" << b << std::endl;std::cout << "c:" << c << std::endl;sleep(10);
}int main() {Thread thread;thread.start(printMessage, "Hello, World!", 42, 1, 2, 3);thread.join();return 0;
}
实现要点
-
完美转发:使用
std::forward保持参数的左值/右值属性 -
任务打包:将可调用对象和参数打包为
std::function<void()> -
内存管理:
-
使用
shared_ptr确保任务对象在线程执行期间有效 -
使用
unique_ptr在线程入口函数中自动释放资源
-
-
线程安全:正确处理线程创建失败时的资源释放
四、直接调用clone系统调用
下面是一个直接使用clone()系统调用的示例:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>#define STACK_SIZE (1024 * 1024) // 1MB栈空间static int child_func(void *arg) {printf("Child process: PID = %d\n", getpid());return 0;
}int main() {// 分配子进程栈空间char *stack = (char*)malloc(STACK_SIZE);if (!stack) {perror("malloc");exit(EXIT_FAILURE);}// 使用clone创建子进程// CLONE_VM: 共享虚拟内存空间// SIGCHLD: 子进程退出时发送SIGCHLD信号pid_t pid = clone(child_func, stack + STACK_SIZE, CLONE_VM | SIGCHLD, NULL);if (pid == -1) {perror("clone");free(stack);exit(EXIT_FAILURE);}printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);// 等待子进程结束if (waitpid(pid, NULL, 0) == -1) {perror("waitpid");free(stack);exit(EXIT_FAILURE);}free(stack);return 0;
}
关键点说明
-
栈分配:
-
必须手动分配栈空间
-
栈指针需要指向分配空间的末尾(因为栈向下增长)
-
-
clone标志:
-
CLONE_VM: 共享虚拟内存空间 -
CLONE_FS/CLONE_FILES: 共享文件系统信息/文件描述符 -
CLONE_SIGHAND: 共享信号处理程序 -
SIGCHLD: 子进程退出时发送SIGCHLD信号
-
-
资源管理:
-
必须手动管理栈内存的分配和释放
-
需要正确处理错误情况下的资源释放
-
-
与pthread_create的区别:
-
更底层,提供更多控制选项
-
需要手动处理更多细节(如栈管理)
-
通常用于实现线程库或特殊需求的进程创建
-
这个示例展示了Linux下线程/进程创建的底层机制,与高层pthread接口形成对比,有助于深入理解Linux的多任务处理机制。
