目录
一、线程栈管理
虽然Linux内核将线程和进程统一在task_struct结构中管理,但在地址空间的栈(stack)管理上仍有显著区别:
进程(主线程)栈管理
-
主线程栈 :可以简单理解为
main()函数的栈空间 -
创建机制 :在
fork()时复制父进程的stack空间地址,采用写时拷贝(COW)机制 -
动态增长:栈空间可以动态增长,但有上限限制
-
溢出处理:
-
超出扩充上限时会触发栈溢出,内核会发送段错误信号(SIGSEGV)给进程
-
进程栈是唯一可以访问未映射页而不一定会立即发生段错误的情况------只有超出扩充上限才会报错
-
子线程栈管理
-
创建方式 :通常通过glibc/uclibc的
pthread_create()接口创建 -
内存分配:
-
使用
mmap系统调用在文件映射区(共享区)分配固定大小的栈空间 -
关键代码(来自glibc的
nptl/allocatestack.c):cppmem = mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); -
默认栈大小通常为8MB(可通过
pthread_attr_setstacksize()调整)
-
-
与进程栈的区别:
-
栈空间是预先固定分配的,不能动态增长
-
用尽栈空间会导致未定义行为(通常是崩溃),而不会像进程栈那样触发动态增长
-
-
系统调用流程 :glibc通过
mmap获取栈内存后,调用sys_clone:cppint 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内部页表两套体系:
页表标志位定义
cpp
/* 页表标志位 */
#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)
关键数据结构
cpp
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; // 页目录起始地址
// ...
};
页表分配函数
页全局目录分配:
cpp
pgd_t *pgd_alloc(struct mm_struct *mm) {
pgd_t *ret = (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO);
// 初始化页表项...
return ret;
}
页表项分配:
cpp
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;
}
三、通用线程封装实现
下面是一个支持任意参数传递的线程封装类实现:
cpp
#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()系统调用的示例:
cpp
#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的多任务处理机制。