欢迎光临
我们一直在努力

C++线程封装与实现详解

1. 线程封装

在理解线程封装的设计与实现之后,我们有必要从系统调用的角度,深入探讨线程的底层实现机制,从而更全面地把握用户层封装与内核实现之间的关联。

// Thread.hpp #pragma once #include <iostream> #include <string> #include <functional> #include <pthread.h> namespace ThreadModule
{
// 原⼦计数器,⽅便形成线程名称
std::uint32_t cnt = 0;

// 线程要执⾏的外部⽅法,我们不考虑传参,后续有std::bind来进⾏类间耦合 using threadfunc_t = std::function<void()>;

// 线程状态 enum class TSTATUS
{
THREAD_NEW,
THREAD_RUNNING,
THREAD_STOP
};

// 线程 class Thread
{
private:
static void *run(void *obj)
{
Thread *self = static_cast<Thread *>(obj);
pthread_setname_np(pthread_self(), self->_name.c_str()); // 设置线程名称
self->_status = TSTATUS::THREAD_RUNNING;
if (!self->_joined)
{
pthread_detach(pthread_self());
}
self->_func();
return nullptr;
}

void SetName()
{
// 后期加锁保护
_name = "Thread-" + std::to_string(cnt++);
}
public:
Thread(threadfunc_t func)
: _status(TSTATUS::THREAD_NEW)
, _joined(true), _func(func)
{
SetName();
}

void EnableDetach()
{
if (_status == TSTATUS::THREAD_NEW) _joined = false;
}

void EnableJoined()
{
if (_status == TSTATUS::THREAD_NEW) _joined = true;
}

bool Start()
{
if (_status == TSTATUS::THREAD_RUNNING) return true;
int n = ::pthread_create(&_id, nullptr, run, this);
if (n != 0) return false;
return true;
}

bool Join()
{
if (_joined)
{
int n = pthread_join(_id, nullptr);
if (n != 0) return false;
return true;
}
return false;
}
~Thread() {}

private:
std::string _name;
pthread_t _id;
TSTATUS _status;
bool _joined;
threadfunc_t _func;
};
}

从本文前半部分的代码可以看出,无论是基于 pthread_create 的线程封装,还是直接使用 clone 系统调用创建线程,本质上都是对内核线程管理机制的一次用户态抽象。在 Linux 系统中,线程本质上是一种“轻量级进程”,它们共享同一进程的地址空间、文件描述符等资源,但拥有独立的执行栈、寄存器上下文和线程控制结构。这种设计使得线程的创建、切换与调度在用户层和内核层之间存在一定的对应关系。

在封装类 Thread 中,我们使用了 pthread_create 来创建线程。该函数在用户层封装了线程的创建逻辑,包括线程栈的分配、线程控制块(TCB)的初始化、线程函数的绑定等。而在内核层,pthread_create 最终会调用 clone 系统调用,通过指定 CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND 等标志,使得新创建的线程与父线程共享地址空间、文件系统信息、文件描述符表和信号处理表,从而实现“线程”的语义。

// main.cc #include <iostream> #include <unistd.h> #include "test.hpp" void hello1()
{
char buffer[64];
pthread_getname_np(pthread_self(), buffer, sizeof(buffer) – 1);
while (true)
{
std::cout << "hello world, " << buffer << std::endl;
sleep(1);
}

}
void hello2()
{
char buffer[64];
pthread_getname_np(pthread_self(), buffer, sizeof(buffer) – 1);
while (true)
{
std::cout << "hello world, " << buffer << std::endl;
sleep(1);
}
}

int main()
{
pthread_setname_np(pthread_self(), "main");
ThreadModule::Thread t1(hello1);
t1.Start();

ThreadModule::Thread t2(std::bind(&hello2));
t2.Start();
t1.Join();
t2.Join();
return 0;
}

• pthread_setname_np 和 pthread_getname_np 是两个⽤于设置和获取线程名称的⾮标准函数(_np 表⽰ "non-portable",即⾮可移植的)。它们通常在 Linux 和其他⼀些类 Unix 系统中可⽤,⽤于调试和多线程程序的管理

• 线程名称⻓度限制: 在 Linux 上,线程名称的最⼤⻓度为 16 个字符(包括结尾的 \\0 )。如果名称超过这个⻓度,会被截断。

• 权限: 通常,只有线程⾃⾝可以设置⾃⼰的名称。尝试设置其他线程的名称可能会导致错误。

// 运⾏结果查询
$ ps -aL
PID LWP TTY TIME CMD
3195828 195828 pts/1 00:00:00 main
195828 195829 pts/1 00:00:00 Thread-0
195828 195830 pts/1 00:00:00 Thread-1

如果要像C++11那样进⾏可变参数的传递,是可以这样设计的,但是太⿇烦了,真到了哪⼀步,就直接⽤c++11吧,我们的⽬标主要是理解系统概念对象化,此处不做复杂设计,⽽且后续可以使⽤std::bind来进⾏对象间调⽤

// 模版形式的 namespace ThreadModule
{
static int number = 1;
enum class TSTATUS
{
NEW,
RUNNING,
STOP
};

template <typename T>
class Thread
{
using func_t = std::function<void(T)>;
private:
// 成员⽅法! static void *Routine(void *args)
{
Thread<T> *t = static_cast<Thread<T> *>(args);
t->_status = TSTATUS::RUNNING;
t->_func(t->_data);
return nullptr;
}

void EnableDetach() { _joinable = false; }

public:
Thread(func_t func, T data) : _func(func), _data(data),
_status(TSTATUS::NEW), _joinable(true)
{
_name = "Thread-" + std::to_string(number++);
_pid = getpid();
}

bool Start()
{
if (_status != TSTATUS::RUNNING)
{
int n = ::pthread_create(&_tid, nullptr, Routine, this); // //TODO if (n != 0)
return false;
return true;
}
return false;
}

bool Stop()
{
if (_status == TSTATUS::RUNNING)
{
int n = ::pthread_cancel(_tid);
if (n != 0)
return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}

bool Join()
{
if (_joinable)
{
int n = ::pthread_join(_tid, nullptr);
if (n != 0) return false;
_status = TSTATUS::STOP;
return true;
}

return false;
}

void Detach()
{
EnableDetach();
pthread_detach(_tid);
}

bool IsJoinable() { return _joinable; }
std::string Name() { return _name; }

~Thread()
{
}

private:
std::string _name;
pthread_t _tid;
pid_t _pid;
bool _joinable; // 是否是分离的,默认不是 func_t _func;
TSTATUS _status;
T _data;
};
}

2. 附录

2.1 源码阅读,理解线程

以下是 glibc-2.4 中 pthread 源码相关内容:

路径:nptl/pthread_create.c

int __pthread_create_2_1(newthread, attr, start_routine, arg)
pthread_t *newthread;
const pthread_attr_t *attr;
void *(*start_routine)(void *);
void *arg;
{
STACK_VARIABLES;
// 重点1: 线程属性,虽然我们不设置,但是不妨碍我们了解
const struct pthread_attr *iattr = (struct pthread_attr *)attr;
if (iattr == NULL)
/* Is this the best idea? On NUMA machines this could mean
accessing far-away memory. */
iattr = &default_attr;
// 重点2:传说中的原⽣线程库中的⽤来描述线程的tcb
struct pthread *pd = NULL;
// 重点3: ALLOCATE_STACK会在先申请struct pthread对象,当然其实是申请⼀⼤块空间,
// struct pthread在空间的开头,⼀会追
int err = ALLOCATE_STACK(iattr, &pd);
if (__builtin_expect(err != 0, 0))
/* Something went wrong. Maybe a parameter of the attributes is
invalid or we could not allocate memory. */
versioned_symbol return err;
/* Initialize the TCB. All initializations with zero should be
performed in 'get_cached_stack'. This way we avoid doing this if
the stack freshly allocated with 'mmap'. */
#ifdef TLS_TCB_AT_TP
/* Reference to the TCB itself. */
pd->header.self = pd;
/* Self-reference for TLS. */
pd->header.tcb = pd;
#endif
/* Store the address of the start routine and the parameter. Since
we do not start the function directly the stillborn thread will
get the information from its thread descriptor. */
// 重点4:向线程tcb中设置未来要执⾏的⽅法的地址和参数
pd->start_routine = start_routine;
pd->arg = arg;
/* Copy the thread attribute flags. */
struct pthread *self = THREAD_SELF;
pd->flags = ((iattr->flags & ~(ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET))
| (self->flags & (ATTR_FLAG_SCHED_SET | ATTR_FLAG_POLICY_SET)));
/* Initialize the field for the ID of the thread which is waiting
for us. This is a self-reference in case the thread is created
detached. */
pd->joinid = iattr->flags & ATTR_FLAG_DETACHSTATE ? pd : NULL;
/* The debug events are inherited from the parent. */
pd->eventbuf = self->eventbuf;

pd->schedpolicy = self->schedpolicy;
pd->schedparam = self->schedparam;
/* Copy the stack guard canary. */
#ifdef THREAD_COPY_STACK_GUARD
THREAD_COPY_STACK_GUARD(pd);
#endif
/* Copy the pointer guard value. */
#ifdef THREAD_COPY_POINTER_GUARD
THREAD_COPY_POINTER_GUARD(pd);
#endif

// ⼀堆参数设定,我们不关⼼
/* Determine scheduling parameters for the thread. */
if (attr != NULL && __builtin_expect((iattr->flags &
ATTR_FLAG_NOTINHERITSCHED) != 0, 0) && (iattr->flags & (ATTR_FLAG_SCHED_SET |
ATTR_FLAG_POLICY_SET)) != 0)
{
INTERNAL_SYSCALL_DECL(scerr);
/* Use the scheduling parameters the user provided. */
if (iattr->flags & ATTR_FLAG_POLICY_SET)
pd->schedpolicy = iattr->schedpolicy;
else if ((pd->flags & ATTR_FLAG_POLICY_SET) == 0)
{
pd->schedpolicy = INTERNAL_SYSCALL(sched_getscheduler, scerr, 1, 0);
pd->flags |= ATTR_FLAG_POLICY_SET;
}
if (iattr->flags & ATTR_FLAG_SCHED_SET)
memcpy(&pd->schedparam, &iattr->schedparam,
sizeof(struct sched_param));
else if ((pd->flags & ATTR_FLAG_SCHED_SET) == 0)
{
INTERNAL_SYSCALL(sched_getparam, scerr, 2, 0, &pd->schedparam);
pd->flags |= ATTR_FLAG_SCHED_SET;
}
/* Check for valid priorities. */
int minprio = INTERNAL_SYSCALL(sched_get_priority_min, scerr, 1,
iattr->schedpolicy);
int maxprio = INTERNAL_SYSCALL(sched_get_priority_max, scerr, 1,
iattr->schedpolicy);
if (pd->schedparam.sched_priority < minprio || pd-
>schedparam.sched_priority > maxprio)
{
err = EINVAL;
goto errout;
}
}

/* Pass the descriptor to the caller. */
// 重点5:把pd(就是线程控制块地址)作为ID,传递出去,所以上层拿到的就是⼀个虚拟地址
*newthread = (pthread_t)pd;
/* Remember whether the thread is detached or not. In case of an
error we have to free the stacks of non-detached stillborn
threads. */
// 重点6: 检测线程属性是否分离,这个很好理解
bool is_detached = IS_DETACHED(pd);
/* Start the thread. */
err = create_thread(pd, iattr, STACK_VARIABLES_ARGS); // 重点函数
if (err != 0)
{
/* Something went wrong. Free the resources. */
if (!is_detached)
{
errout:
__deallocate_stack(pd);
}
return err;
}
return 0;
}

// 版本确认信息,意思就是如果⽤的库是GLIBC_2_1,pthread_create函数就是
__pthread_create_2_1
versioned_symbol(libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1);

线程栈的管理是线程实现中的核心环节。从本文中的代码和源码分析可以看出,线程栈的分配主要有两种方式:

  • 用户指定栈:通过 pthread_attr_t 结构体可以设置自定义的栈地址和大小;

  • 系统自动分配:若不指定,则由 allocate_stack 函数在堆或内存映射区中动态分配。

  • 在 allocate_stack 函数中,系统会优先尝试从线程栈缓存中获取合适的栈空间,若缓存中无合适空间,则通过 mmap 系统调用在进程的地址空间中映射一段匿名内存作为栈使用。这段内存通常位于进程的“堆”与“共享库”之间的区域,且默认大小通常为 8MB(可通过 ulimit -s 调整)。

    值得注意的是,线程栈一旦分配,其大小便是固定的,不支持动态扩展。这与主线程(进程)的栈不同,后者可通过内核的“自动扩展”机制动态增长。因此,线程栈溢出是编程中常见的错误,一旦栈空间耗尽,程序将立即收到段错误信号。

    线程属性:

    struct pthread_attr
    {
    /* Scheduler parameters and priority. */
    struct sched_param schedparam;
    int schedpolicy;
    /* Various flags like detachstate, scope, etc. */
    int flags;
    /* Size of guard area. */
    size_t guardsize;
    /* Stack handling. */
    void *stackaddr;
    size_t stacksize;
    /* Affinity map. */
    cpu_set_t *cpuset;
    size_t cpusetsize;
    };

    线程tcb:

    /* Thread descriptor data structure. */
    struct pthread
    {
    union
    {
    #if !TLS_DTV_AT_TP
    /* This overlaps the TCB as used for TLS without threads (see tls.h). */
    tcbhead_t header;
    #else
    struct
    {
    int multiple_threads;
    } header;
    #endif
    /* This extra padding has no special purpose, and this structure layout
    is private and subject to change without affecting the official ABI.
    We just have it here in case it might be convenient for some
    implementation-specific instrumentation hack or suchlike. */
    void *__padding[16];
    };

    pid_t tid;
    /* Process ID – thread group ID in kernel speak. */
    pid_t pid;
    /* List of robust mutexes the thread is holding. */
    #ifdef __PTHREAD_MUTEX_HAVE_PREV
    __pthread_list_t robust_list;
    # define ENQUEUE_MUTEX(mutex) \\ do { \\
    __pthread_list_t *next = THREAD_GETMEM (THREAD_SELF, robust_list.__next);
    \\
    next->__prev = &mutex->__data.__list; \\
    mutex->__data.__list.__next = next; \\
    mutex->__data.__list.__prev = &THREAD_SELF->robust_list; \\
    THREAD_SETMEM (THREAD_SELF, robust_list.__next, &mutex->__data.__list);
    \\
    } while (0)
    # define DEQUEUE_MUTEX(mutex) \\ do { \\
    mutex->__data.__list.__next->__prev = mutex->__data.__list.__prev;
    \\
    mutex->__data.__list.__prev->__next = mutex->__data.__list.__next;
    \\
    mutex->__data.__list.__prev = NULL; \\
    mutex->__data.__list.__next = NULL; \\
    } while (0)
    #else
    __pthread_slist_t robust_list;
    # define ENQUEUE_MUTEX(mutex) \\ do { \\
    mutex->__data.__list.__next \\
    = THREAD_GETMEM (THREAD_SELF, robust_list.__next); \\
    THREAD_SETMEM (THREAD_SELF, robust_list.__next, &mutex->__data.__list);
    \\
    } while (0)
    # define DEQUEUE_MUTEX(mutex) \\ do { \\
    __pthread_slist_t *runp = THREAD_GETMEM (THREAD_SELF,
    robust_list.__next);\\
    if (runp == &mutex->__data.__list) \\
    THREAD_SETMEM (THREAD_SELF, robust_list.__next, runp->__next); \\
    else \\
    { \\
    while (runp->__next != &mutex->__data.__list) \\
    runp = runp->__next; \\
    \\
    runp->__next = runp->__next->__next; \\
    mutex->__data.__list.__next = NULL; \\
    } \\
    } while (0)
    #endif
    /* List of cleanup buffers. */
    struct _pthread_cleanup_buffer *cleanup;
    /* Unwind information. */
    struct pthread_unwind_buf *cleanup_jmp_buf;
    #define HAVE_CLEANUP_JMP_BUF
    /* Flags determining processing of cancellation. */
    int cancelhandling;
    /* Bit set if cancellation is disabled. */
    #define CANCELSTATE_BIT 0 #define CANCELSTATE_BITMASK 0x01
    /* Bit set if asynchronous cancellation mode is selected. */
    #define CANCELTYPE_BIT 1 #define CANCELTYPE_BITMASK 0x02
    /* Bit set if canceling has been initiated. */
    #define CANCELING_BIT 2 #define CANCELING_BITMASK 0x04
    /* Bit set if canceled. */
    #define CANCELED_BIT 3 #define CANCELED_BITMASK 0x08
    /* Bit set if thread is exiting. */
    #define EXITING_BIT 4 #define EXITING_BITMASK 0x10
    /* Bit set if thread terminated and TCB is freed. */
    #define TERMINATED_BIT 5 #define TERMINATED_BITMASK 0x20
    /* Bit set if thread is supposed to change XID. */
    #define SETXID_BIT 6 #define SETXID_BITMASK 0x40
    /* Mask for the rest. Helps the compiler to optimize. */
    #define CANCEL_RESTMASK 0xffffff80 #define CANCEL_ENABLED_AND_CANCELED(value) \\
    (((value) & (CANCELSTATE_BITMASK | CANCELED_BITMASK | EXITING_BITMASK
    \\
    | CANCEL_RESTMASK | TERMINATED_BITMASK)) == CANCELED_BITMASK)
    #define CANCEL_ENABLED_AND_CANCELED_AND_ASYNCHRONOUS(value) \\
    (((value) & (CANCELSTATE_BITMASK | CANCELTYPE_BITMASK | CANCELED_BITMASK
    \\
    | EXITING_BITMASK | CANCEL_RESTMASK | TERMINATED_BITMASK)) \\
    == (CANCELTYPE_BITMASK | CANCELED_BITMASK))
    /* We allocate one block of references here. This should be enough
    to avoid allocating any memory dynamically for most applications. */
    struct pthread_key_data
    {
    /* Sequence number. We use uintptr_t to not require padding on
    32- and 64-bit machines. On 64-bit machines it helps to avoid
    wrapping, too. */
    uintptr_t seq;
    /* Data pointer. */
    void *data;
    } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
    /* Two-level array for the thread-specific data. */
    struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
    /* Flag which is set when specific data is set. */
    bool specific_used;
    /* True if events must be reported. */
    bool report_events;
    /* True if the user provided the stack. */
    bool user_stack;
    /* True if thread must stop at startup time. */
    bool stopped_start;
    /* Lock to synchronize access to the descriptor. */
    lll_lock_t lock;
    /* Lock for synchronizing setxid calls. */
    lll_lock_t setxid_futex;
    #if HP_TIMING_AVAIL
    /* Offset of the CPU clock at start thread start time. */
    hp_timing_t cpuclock_offset;
    #endif
    /* If the thread waits to join another one the ID of the latter is
    stored here.
    In case a thread is detached this field contains a pointer of the
    TCB if the thread itself. This is something which cannot happen
    in normal operation. */
    struct pthread *joinid;
    /* Check whether a thread is detached. */
    #define IS_DETACHED(pd) ((pd)->joinid == (pd))
    /* Flags. Including those copied from the thread attribute. */
    int flags;
    /* The result of the thread function. */
    // 线程运⾏完毕,返回值就是void*, 最后的返回值就放在tcb中的该变量⾥⾯
    // 所以我们⽤pthread_join获取线程退出信息的时候,就是读取该结构体
    // 另外,要能理解线程执⾏流可以退出,但是tcb可以暂时保留,这句话
    void *result;
    /* Scheduling parameters for the new thread. */
    struct sched_param schedparam;
    int schedpolicy;
    /* Start position of the code to be executed and the argument passed
    to the function. */
    // ⽤⼾指定的⽅法和参数
    void *(*start_routine) (void *);
    void *arg;
    /* Debug state. */
    td_eventbuf_t eventbuf;
    /* Next descriptor with a pending event. */
    struct pthread *nextevent;
    #ifdef HAVE_FORCED_UNWIND
    /* Machine-specific unwind info. */
    struct _Unwind_Exception exc;
    #endif
    /* If nonzero pointer to area allocated for the stack and its
    size. */
    // 线程⾃⼰的栈和⼤⼩
    void *stackblock;
    size_t stackblock_size;
    /* Size of the included guard area. */
    size_t guardsize;
    /* This is what the user specified and what we will report. */
    size_t reported_guardsize;
    /* Resolver state. */
    struct __res_state res;
    /* This member must be last. */
    char end_padding[];
    #define PTHREAD_STRUCT_END_PADDING \\
    (sizeof (struct pthread) – offsetof (struct pthread, end_padding))
    } __attribute ((aligned (TCB_ALIGNMENT)));

    在 glibc 的实现中,每个线程都有一个对应的 struct pthread 结构体,即线程控制块(TCB)。该结构体存储了线程的所有元信息,包括:

    • 线程 ID(tid)、进程 ID(pid)

    • 栈基址与栈大小

    • 线程局部存储(TLS)相关信息

    • 线程调度策略与优先级

    • 线程取消状态与清理函数链

    • 线程返回值指针等

    TCB 通常被放置在栈空间的末尾(或顶部,取决于架构),这样既方便通过栈指针快速定位,也便于在栈释放时一并回收。在 pthread_create 中,ALLOCATE_STACK 宏不仅分配了栈空间,还在其中预留了 TCB 的位置,并通过 pd 指针返回其地址。

    create_thread

    tatic int
    create_thread(struct pthread *pd, const struct pthread_attr *attr,
    STACK_VARIABLES_PARMS)
    {
    #ifdef TLS_TCB_AT_TP
    assert(pd->header.tcb != NULL);
    #endif
    /* We rely heavily on various flags the CLONE function understands:
    CLONE_VM, CLONE_FS, CLONE_FILES
    These flags select semantics with shared address space and
    file descriptors according to what POSIX requires.
    CLONE_SIGNAL
    This flag selects the POSIX signal semantics.
    CLONE_SETTLS
    The sixth parameter to CLONE determines the TLS area for the
    new thread.
    CLONE_PARENT_SETTID
    The kernels writes the thread ID of the newly created thread
    into the location pointed to by the fifth parameters to CLONE.
    Note that it would be semantically equivalent to use
    CLONE_CHILD_SETTID but it is be more expensive in the kernel.
    CLONE_CHILD_CLEARTID
    The kernels clears the thread ID of a thread that has called
    sys_exit() in the location pointed to by the seventh parameter
    to CLONE.
    CLONE_DETACHED
    No signal is generated if the thread exists and it is
    automatically reaped.
    The termination signal is chosen to be zero which means no signal
    is sent. */
    int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL |
    CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
    #if __ASSUME_NO_CLONE_DETACHED == 0
    | CLONE_DETACHED
    #endif
    | 0);
    if (__builtin_expect(THREAD_GETMEM(THREAD_SELF, report_events), 0))
    {
    /* The parent thread is supposed to report events. Check whether
    the TD_CREATE event is needed, too. */
    const int _idx = __td_eventword(TD_CREATE);
    const uint32_t _mask = __td_eventmask(TD_CREATE);
    if ((_mask & (__nptl_threads_events.event_bits[_idx] | pd-
    >eventbuf.eventmask.event_bits[_idx])) != 0)
    {
    /* We always must have the thread start stopped. */
    pd->stopped_start = true;
    /* Create the thread. We always create the thread stopped
    so that it does not get far before we tell the debugger. */
    int res = do_clone(pd, attr, clone_flags, start_thread,
    STACK_VARIABLES_ARGS, 1);
    if (res == 0)
    {
    /* Now fill in the information about the new thread in
    the newly created thread's data structure. We cannot let
    the new thread do this since we don't know whether it was
    already scheduled when we send the event. */
    pd->eventbuf.eventnum = TD_CREATE;
    pd->eventbuf.eventdata = pd;
    /* Enqueue the descriptor. */
    do
    pd->nextevent = __nptl_last_event;
    while (atomic_compare_and_exchange_bool_acq(&__nptl_last_event,
    pd, pd->nextevent) != 0);
    /* Now call the function which signals the event. */
    __nptl_create_event();
    /* And finally restart the new thread. */
    lll_unlock(pd->lock);
    }
    return res;
    }
    }
    #ifdef NEED_DL_SYSINFO
    assert(THREAD_SELF_SYSINFO == THREAD_SYSINFO(pd));
    #endif
    /* Determine whether the newly created threads has to be started
    stopped since we have to set the scheduling parameters or set the
    affinity. */
    bool stopped = false;
    if (attr != NULL && (attr->cpuset != NULL || (attr->flags &
    ATTR_FLAG_NOTINHERITSCHED) != 0))
    stopped = true;
    pd->stopped_start = stopped;
    /* Actually create the thread. */
    int res = do_clone(pd, attr, clone_flags, start_thread,
    STACK_VARIABLES_ARGS, stopped);
    if (res == 0 && stopped)
    /* And finally restart the new thread. */
    lll_unlock(pd->lock);
    return res;
    }

    从 clone 系统调用进入内核后,内核会调用 do_fork(或 kernel_clone)来创建新的任务结构体 task_struct。由于指定了共享地址空间等标志,新创建的 task_struct 会与父任务共享 mm_struct(内存描述符),从而实现了线程间的内存共享。

    在内核中,线程与进程的调度并无本质区别,均由调度器统一管理。每个线程作为一个独立的调度实体,拥有自己的 task_struct,参与进程调度。这也意味着,线程的切换与进程切换在底层机制上是相似的,都涉及寄存器上下文的保存与恢复、页表的切换等操作。

    do_clone

    static int
    do_clone(struct pthread *pd, const struct pthread_attr *attr,
    int clone_flags, int (*fct)(void *), STACK_VARIABLES_PARMS,
    int stopped)
    {
    #ifdef PREPARE_CREATE
    PREPARE_CREATE;
    #endif if (stopped)
    /* We Make sure the thread does not run far by forcing it to get a
    lock. We lock it here too so that the new thread cannot continue
    until we tell it to. */
    lll_lock(pd->lock);
    /* One more thread. We cannot have the thread do this itself, since it
    might exist but not have been scheduled yet by the time we've returned
    and need to check the value to behave correctly. We must do it before
    creating the thread, in case it does get scheduled first and then
    might mistakenly think it was the only thread. In the failure case,
    we momentarily store a false value; this doesn't matter because there
    is no kosher thing a signal handler interrupting us right here can do
    that cares whether the thread count is correct. */
    atomic_increment(&__nptl_nthreads);
    // 执⾏特性体系结构下的clone函数
    if (ARCH_CLONE(fct, STACK_VARIABLES_ARGS, clone_flags,
    pd, &pd->tid, TLS_VALUE, &pd->tid) == -1)
    {
    atomic_decrement(&__nptl_nthreads); /* Oops, we lied for a second. */
    /* Failed. If the thread is detached, remove the TCB here since
    the caller cannot do this. The caller remembered the thread
    as detached and cannot reverify that it is not since it must
    not access the thread descriptor again. */
    if (IS_DETACHED(pd))
    __deallocate_stack(pd);
    return errno;
    }
    /* Now we have the possibility to set scheduling parameters etc. */
    // 下⾯是调⽤相关系统调⽤,设置轻量级进程的调度参数和⼀些异常处理,不关⼼
    if (__builtin_expect(stopped != 0, 0))
    {
    INTERNAL_SYSCALL_DECL(err);
    int res = 0;
    /* Set the affinity mask if necessary. */
    if (attr->cpuset != NULL)
    {
    res = INTERNAL_SYSCALL(sched_setaffinity, err, 3, pd->tid,
    sizeof(cpu_set_t), attr->cpuset);
    if (__builtin_expect(INTERNAL_SYSCALL_ERROR_P(res, err), 0))
    {
    /* The operation failed. We have to kill the thread. First
    send it the cancellation signal. */
    INTERNAL_SYSCALL_DECL(err2);
    err_out:
    #if __ASSUME_TGKILL
    (void)INTERNAL_SYSCALL(tgkill, err2, 3,
    THREAD_GETMEM(THREAD_SELF, pid),
    pd->tid, SIGCANCEL);
    #else
    (void)INTERNAL_SYSCALL(tkill, err2, 2, pd->tid, SIGCANCEL);
    #endif return (INTERNAL_SYSCALL_ERROR_P(res, err)
    ? INTERNAL_SYSCALL_ERRNO(res, err)
    : 0);
    }
    }
    /* Set the scheduling parameters. */
    if ((attr->flags & ATTR_FLAG_NOTINHERITSCHED) != 0)
    {
    res = INTERNAL_SYSCALL(sched_setscheduler, err, 3, pd->tid,
    pd->schedpolicy, &pd->schedparam);
    if (__builtin_expect(INTERNAL_SYSCALL_ERROR_P(res, err), 0))
    goto err_out;
    }
    }
    /* We now have for sure more than one thread. The main thread might
    not yet have the flag set. No need to set the global variable
    again if this is what we use. */
    THREAD_SETMEM(THREAD_SELF, header.multiple_threads, 1);
    return 0;
    }

    尽管线程的最终调度由内核完成,但在用户层我们仍可以构建更高级的调度机制,例如协程(coroutine)或用户态线程。这类实现通常依赖于:

  • 使用 setjmp/longjmp 或汇编实现上下文切换;

  • 自行管理协程栈的分配与切换;

  • 在单一线程内实现多个执行流的调度。

  • 这种方式的优势在于切换开销小、调度可控,适用于高并发 I/O 密集型场景。典型的实现如 libco、Boost.Coroutine 等。它们本质上是在用户层模拟了线程的切换逻辑,但避免了频繁的内核态与用户态切换

    #define ARCH_CLONE __clone
    __clone是glibc⽤汇编封装的⼀个调⽤clone系统调⽤的函数,所以
    __clone的实现就是汇编,贴⼀份代码(sysdeps/unix/sysv/linux/x86_64):
    ENTRY (BP_SYM (__clone))
    /* Sanity check arguments. */
    movq $-EINVAL,%rax
    testq %rdi,%rdi /* no NULL function pointers */
    jz SYSCALL_ERROR_LABEL
    testq %rsi,%rsi /* no NULL stack pointers */
    jz SYSCALL_ERROR_LABEL
    /* Insert the argument onto the new stack. */
    subq $16,%rsi
    movq %rcx,8(%rsi)
    /* Save the function pointer. It will be popped off in the
    child in the ebx frobbing below. */
    movq %rdi,0(%rsi)
    /* Do the system call. */
    movq %rdx, %rdi
    movq %r8, %rdx
    movq %r9, %r8
    movq 8(%rsp), %r10
    movl $SYS_ify(clone),%eax // 获取系统调⽤号
    /* End FDE now, because in the child the unwind info will be
    wrong. */
    cfi_endproc;
    syscall // 陷⼊内核(x86_32是int 80),要求内核创建轻量级进程
    testq %rax,%rax
    jl SYSCALL_ERROR_LABEL
    jz L(thread_start)
    这部分代码了解即可。

    通过对 glibc 线程实现的分析,我们可以得到以下设计启示:

    • 封装应隐藏底层复杂性:用户无需关心栈如何分配、TCB 如何布局,只需关注线程函数与同步机制;

    • 资源管理应自动化:线程栈、TCB 等资源应在生命周期结束时自动释放,避免内存泄漏;

    • 接口应具备扩展性:支持多种函数签名、参数传递方式,如使用 std::function 与模板实现泛型绑定;

    • 性能与安全并重:线程栈应有保护页(guard page)防止溢出,调度策略应支持优先级设置等。

    下⾯我们追⼀下空间申请: int err = ALLOCATE_STACK(iattr, &pd);

    源码路径:nptl/allocatestack.c
    //空间申请的函数,其实就是⼀个宏
    #define ALLOCATE_STACK(attr, pd) \\
    allocate_stack(attr, pd, &stackaddr, &stacksize)
    static int
    allocate_stack(const struct pthread_attr *attr, struct pthread **pdp,
    ALLOCATE_STACK_PARMS)
    {
    struct pthread *pd;
    size_t size;
    size_t pagesize_m1 = __getpagesize() – 1;
    void *stacktop;
    assert(attr != NULL);
    assert(powerof2(pagesize_m1 + 1));
    assert(TCB_ALIGNMENT >= STACK_ALIGN);
    /* Get the stack size from the attribute if it is set. Otherwise we
    use the default we determined at start time. */
    size = attr->stacksize ?: __default_stacksize; // 获取栈⼤⼩,⽤⼾没设置就默认
    /* Get memory for the stack. */
    // 如果已经⽤⼾已经在线程属性⾥⾯设置了空间,就直接⽤,我们是默认,这部分代码直接不看
    if (__builtin_expect(attr->flags & ATTR_FLAG_STACKADDR, 0))
    {
    uintptr_t adj;
    /* If the user also specified the size of the stack make sure it
    is large enough. */
    if (attr->stacksize != 0 && attr->stacksize < (__static_tls_size +
    MINIMAL_REST_STACK))
    return EINVAL;
    /* Adjust stack size for alignment of the TLS block. */
    #if TLS_TCB_AT_TP
    adj = ((uintptr_t)attr->stackaddr – TLS_TCB_SIZE) & __static_tls_align_m1;
    assert(size > adj + TLS_TCB_SIZE);
    #elif TLS_DTV_AT_TP
    adj = ((uintptr_t)attr->stackaddr – __static_tls_size) &
    __static_tls_align_m1;
    assert(size > adj);
    #endif
    /* The user provided some memory. Let's hope it matches the
    size… We do not allocate guard pages if the user provided
    the stack. It is the user's responsibility to do this if it
    is wanted. */
    #if TLS_TCB_AT_TP
    pd = (struct pthread *)((uintptr_t)attr->stackaddr – TLS_TCB_SIZE – adj);
    #elif TLS_DTV_AT_TP
    pd = (struct pthread *)(((uintptr_t)attr->stackaddr – __static_tls_size –
    adj) – TLS_PRE_TCB_SIZE);
    #endif
    /* The user provided stack memory needs to be cleared. */
    memset(pd, '\\0', sizeof(struct pthread));
    /* The first TSD block is included in the TCB. */
    pd->specific[0] = pd->specific_1stblock;
    /* Remember the stack-related values. */
    pd->stackblock = (char *)attr->stackaddr – size;
    pd->stackblock_size = size;
    /* This is a user-provided stack. It will not be queued in the
    stack cache nor will the memory (except the TLS memory) be freed. */
    pd->user_stack = true;
    /* This is at least the second thread. */
    pd->header.multiple_threads = 1;
    #ifndef TLS_MULTIPLE_THREADS_IN_TCB
    __pthread_multiple_threads = *__libc_multiple_threads_ptr = 1;
    #endif #ifdef NEED_DL_SYSINFO
    /* Copy the sysinfo value from the parent. */
    THREAD_SYSINFO(pd) = THREAD_SELF_SYSINFO;
    #endif
    /* The process ID is also the same as that of the caller. */
    pd->pid = THREAD_GETMEM(THREAD_SELF, pid);
    /* List of robust mutexes. */
    #ifdef __PTHREAD_MUTEX_HAVE_PREV
    pd->robust_list.__prev = &pd->robust_list;
    #endif
    pd->robust_list.__next = &pd->robust_list;
    /* Allocate the DTV for this thread. */
    if (_dl_allocate_tls(TLS_TPADJ(pd)) == NULL)
    {
    /* Something went wrong. */
    assert(errno == ENOMEM);
    return EAGAIN;
    }
    /* Prepare to modify global data. */
    lll_lock(stack_cache_lock);
    /* And add to the list of stacks in use. */
    list_add(&pd->list, &__stack_user);
    lll_unlock(stack_cache_lock);
    }
    else
    {
    // 下⾯的都是库内部⾃⼰做的,我们关⼼的
    /* Allocate some anonymous memory. If possible use the cache. */
    size_t guardsize;
    size_t reqsize;
    void *mem;
    const int prot = (PROT_READ | PROT_WRITE | ((GL(dl_stack_flags) & PF_X) ?
    PROT_EXEC : 0));
    #if COLORING_INCREMENT != 0 if (size <= 16 * pagesize_m1)
    size += pagesize_m1 + 1;
    #endif
    /* Adjust the stack size for alignment. */
    size &= ~__static_tls_align_m1; // 设置空间对⻬
    assert(size != 0);
    /* Make sure the size of the stack is enough for the guard and
    eventually the thread descriptor. */
    guardsize = (attr->guardsize + pagesize_m1) & ~pagesize_m1;
    if (__builtin_expect(size < ((guardsize + __static_tls_size +
    MINIMAL_REST_STACK + pagesize_m1) & ~pagesize_m1),
    0))
    /* The stack is too small (or the guard too large). */
    return EINVAL;
    /* Try to get a stack from the cache. */
    // 先尝试从pthread缓存中申请空间
    reqsize = size;
    pd = get_cached_stack(&size, &mem);
    if (pd == NULL)
    {
    /* To avoid aliasing effects on a larger scale than pages we
    adjust the allocated stack size if necessary. This way
    allocations directly following each other will not have
    aliasing problems. */
    #if MULTI_PAGE_ALIASING != 0 if ((size % MULTI_PAGE_ALIASING) == 0)
    size += pagesize_m1 + 1;
    #endif
    // 缓存申请失败,就在堆空间申请私有的匿名内存空间,这⾥mmap类似malloc
    // 当然他也可以作为共享内存的实现,类似原理我们接触过,这个功能和当前⽆关
    mem = mmap(NULL, size, prot,
    MAP_PRIVATE | MAP_ANONYMOUS | ARCH_MAP_FLAGS, -1, 0);
    if (__builtin_expect(mem == MAP_FAILED, 0))
    {
    #ifdef ARCH_RETRY_MMAP
    mem = ARCH_RETRY_MMAP(size);
    if (__builtin_expect(mem == MAP_FAILED, 0))
    #endif return errno;
    }
    /* SIZE is guaranteed to be greater than zero.
    So we can never get a null pointer back from mmap. */
    assert(mem != NULL);
    #if COLORING_INCREMENT != 0
    /* Atomically increment NCREATED. */
    unsigned int ncreated = atomic_increment_val(&nptl_ncreated);
    /* We chose the offset for coloring by incrementing it for
    every new thread by a fixed amount. The offset used
    module the page size. Even if coloring would be better
    relative to higher alignment values it makes no sense to
    do it since the mmap() interface does not allow us to
    specify any alignment for the returned memory block. */
    size_t coloring = (ncreated * COLORING_INCREMENT) & pagesize_m1;
    /* Make sure the coloring offsets does not disturb the alignment
    of the TCB and static TLS block. */
    if (__builtin_expect((coloring & __static_tls_align_m1) != 0, 0))
    coloring = (((coloring + __static_tls_align_m1) & ~
    (__static_tls_align_m1)) & ~pagesize_m1);
    #else
    /* Unless specified we do not make any adjustments. */
    #define coloring 0 #endif
    /* Place the thread descriptor at the end of the stack. */
    // 下⾯的代码其实就是我们课件中的图,这⾥是在申请的空间中确定struct
    thread(tcb)的地址
    #if TLS_TCB_AT_TP
    pd = (struct pthread *)((char *)mem + size – coloring) – 1;
    #elif TLS_DTV_AT_TP
    pd = (struct pthread *)((((uintptr_t)mem + size – coloring –
    __static_tls_size) & ~__static_tls_align_m1) – TLS_PRE_TCB_SIZE);
    #endif
    /* Remember the stack-related values. */
    // 记录下来整个空间的地址和⼤⼩
    pd->stackblock = mem;
    pd->stackblock_size = size;
    /* We allocated the first block thread-specific data array.
    This address will not change for the lifetime of this
    descriptor. */
    pd->specific[0] = pd->specific_1stblock;
    /* This is at least the second thread. */
    pd->header.multiple_threads = 1;
    #ifndef TLS_MULTIPLE_THREADS_IN_TCB
    __pthread_multiple_threads = *__libc_multiple_threads_ptr = 1;
    #endif
    #ifdef NEED_DL_SYSINFO
    /* Copy the sysinfo value from the parent. */
    THREAD_SYSINFO(pd) = THREAD_SELF_SYSINFO;
    #endif
    /* The process ID is also the same as that of the caller. */
    // 获取线程对应进程的pid
    pd->pid = THREAD_GETMEM(THREAD_SELF, pid);
    /* List of robust mutexes. */
    #ifdef __PTHREAD_MUTEX_HAVE_PREV
    pd->robust_list.__prev = &pd->robust_list;
    #endif
    pd->robust_list.__next = &pd->robust_list;
    /* Allocate the DTV for this thread. */
    if (_dl_allocate_tls(TLS_TPADJ(pd)) == NULL)
    {
    /* Something went wrong. */
    assert(errno == ENOMEM);
    /* Free the stack memory we just allocated. */
    (void)munmap(mem, size);
    return EAGAIN;
    }
    /* Prepare to modify global data. */
    lll_lock(stack_cache_lock);
    /* And add to the list of stacks in use. */
    list_add(&pd->list, &stack_used);
    lll_unlock(stack_cache_lock);
    /* There might have been a race. Another thread might have
    caused the stacks to get exec permission while this new
    stack was prepared. Detect if this was possible and
    change the permission if necessary. */
    if (__builtin_expect((GL(dl_stack_flags) & PF_X) != 0 && (prot &
    PROT_EXEC) == 0, 0))
    {
    int err = change_stack_perm(pd
    #ifdef NEED_SEPARATE_REGISTER_STACK
    ,
    ~pagesize_m1
    #endif
    );
    if (err != 0)
    {
    /* Free the stack memory we just allocated. */
    (void)munmap(mem, size);
    return err;
    }
    }
    /* Note that all of the stack and the thread descriptor is
    zeroed. This means we do not have to initialize fields
    with initial value zero. This is specifically true for
    the 'tid' field which is always set back to zero once the
    stack is not used anymore and for the 'guardsize' field
    which will be read next. */
    }
    /* Create or resize the guard area if necessary. */
    if (__builtin_expect(guardsize > pd->guardsize, 0))
    {
    #ifdef NEED_SEPARATE_REGISTER_STACK
    char *guard = mem + (((size – guardsize) / 2) & ~pagesize_m1);
    #else
    char *guard = mem;
    #endif
    if (mprotect(guard, guardsize, PROT_NONE) != 0)
    {
    int err;
    mprot_error:
    err = errno;
    lll_lock(stack_cache_lock);
    /* Remove the thread from the list. */
    list_del(&pd->list);
    lll_unlock(stack_cache_lock);
    /* Get rid of the TLS block we allocated. */
    _dl_deallocate_tls(TLS_TPADJ(pd), false);
    /* Free the stack memory regardless of whether the size
    of the cache is over the limit or not. If this piece
    of memory caused problems we better do not use it
    anymore. Uh, and we ignore possible errors. There
    is nothing we could do. */
    (void)munmap(mem, size);
    return err;
    }
    pd->guardsize = guardsize;
    }
    else if (__builtin_expect(pd->guardsize – guardsize > size – reqsize,
    0))
    {
    /* The old guard area is too large. */
    #ifdef NEED_SEPARATE_REGISTER_STACK
    char *guard = mem + (((size – guardsize) / 2) & ~pagesize_m1);
    char *oldguard = mem + (((size – pd->guardsize) / 2) & ~pagesize_m1);
    if (oldguard < guard && mprotect(oldguard, guard – oldguard, prot) != 0)
    goto mprot_error;
    if (mprotect(guard + guardsize,
    oldguard + pd->guardsize – guard – guardsize,
    prot) != 0)
    goto mprot_error;
    #else
    if (mprotect((char *)mem + guardsize, pd->guardsize – guardsize,
    prot) != 0)
    goto mprot_error;
    #endif
    pd->guardsize = guardsize;
    }
    /* The pthread_getattr_np() calls need to get passed the size
    requested in the attribute, regardless of how large the
    actually used guardsize is. */
    pd->reported_guardsize = guardsize;
    }
    /* Initialize the lock. We have to do this unconditionally since the
    stillborn thread could be canceled while the lock is taken. */
    pd->lock = LLL_LOCK_INITIALIZER;
    /* We place the thread descriptor at the end of the stack. */
    // ⼆级指针,返回struct thread的地址,其实就是⼀个堆快的地址,对⽐之前的⽰意图
    *pdp = pd;
    #if TLS_TCB_AT_TP
    /* The stack begins before the TCB and the static TLS block. */
    stacktop = ((char *)(pd + 1) – __static_tls_size);
    #elif TLS_DTV_AT_TP
    stacktop = (char *)(pd – 1);
    #endif
    #ifdef NEED_SEPARATE_REGISTER_STACK
    *stack = pd->stackblock;
    *stacksize = stacktop – *stack;
    #else
    *stack = stacktop;
    #endif
    return 0;
    }
    // 所以,在创建线程的时候,其实就是在pthread库内部,创建好描述线程的结构体对象,填充属性
    // 第⼆步就是调⽤clone,让内核创建轻量级进程,并执⾏传⼊的回调函数和参数
    // 其实,库提供的⽆⾮就是未来操作线程的API,通过属性设置线程的优先级之类,⽽真正调度的
    // 过程,还是内核来的。
    // 但是如果我们⾃⼰在上层,设计⼀些让线程暂停出让CPU,然后我们上次⾃定义队列,让线程的
    tcb进⾏排队
    // 那么我们其实也可以基于内核,在⽤⼾层实现线程的调度,很多更⾼级的语⾔,可能会做这个⼯
    作。

    /* How to pass the values to the 'create_thread' function. */

    #d#define STACK_VARIABLES_ARGS stackaddr // STACK_VARIABLES_ARGS

    其实就是stack地址

    2.2 线程栈

    虽然 Linux 将线程和进程不加区分的统⼀到了 task_struct,但是对待其地址空间的 stack 还是有些区别的。

    • 对于 Linux 进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了⽗亲的 stack 空间地址,然后写时拷⻉(cow)以及动态增⻓。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错误⸺超出扩充上限才报。

    • 然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack 将不再是向下⽣⻓的,⽽是事先固定下来的。线程栈⼀般是调⽤glibc/uclibc等的 pthread 库接⼝ pthread_create 创建的线程,在⽂件映射区(或称之为共享区)。其中使⽤ mmap 系统调⽤,这个可以从 glibc的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:

    mem = mmap (NULL, size, prot,
    MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

    此调⽤中的 size 参数的获取很是复杂,你可以⼿⼯传⼊stack的⼤⼩,也可以使⽤默认的,⼀般⽽⾔就是默认的 8M 。这些都不重要,重要的是,这种stack不能动态增⻓,⼀旦⽤尽就没了,这是和⽣成进程的fork不同的地⽅。在glibc中通过mmap得到了stack之后,底层将调⽤ sys_clone 系统调⽤

    int sys_clone(struct pt_regs *regs)
    {
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;

    clone_flags = regs->bx;
    //获取了mmap得到的线程的stack指针
    newsp = regs->cx;
    parent_tidptr = (int __user *)regs->dx;
    child_tidptr = (int __user *)regs->di;
    if (!newsp)
    newsp = regs->sp;
    return do_fork(clone_flags, newsp, regs, 0, parent_tidptr,
    child_tidptr);
    }

    因此,对于⼦线程的 stack ,它其实是在进程的地址空间中map出来的⼀块内存区域,原则上是线程私有的,但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct 的很多字段,如果愿意,其它线程也还是可以访问到的,⼀定要注意。

    2.3 ⻚表和⻚表项

    * We keep two sets of PTEs – the hardware and the linux version.
    * This allows greater flexibility in the way we map the Linux bits
    * onto the hardware tables, and allows us to have YOUNG and DIRTY
    * bits.
    *
    * The PTE table pointer refers to the hardware entries; the "Linux"
    * entries are stored 1024 bytes below.
    */
    // ⻚表标志位
    #define L_PTE_PRESENT (1 << 0) #define L_PTE_FILE (1 << 1) /* only when !PRESENT */ #define L_PTE_YOUNG (1 << 1) #define L_PTE_BUFFERABLE (1 << 2) /* matches PTE */ #define L_PTE_CACHEABLE (1 << 3) /* matches PTE */ #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 coherent (xsc3) */ #define L_PTE_SHARED (1 << 10) /* shared between CPUs (v6) */ #define L_PTE_ASID (1 << 11) /* non-global (use ASID, v6) */
    // ⻚表是?
    typedef struct { unsigned long pte; } pte_t; // ⻚表项
    typedef struct { unsigned long pgd; } pgd_t; // ⻚全局⽬录项

    pgd_t *
    pgd_alloc(struct mm_struct *mm)
    {
    pgd_t *ret, *init;
    ret = (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO);
    init = pgd_offset(&init_mm, 0UL);
    if (ret) {
    #ifdef CONFIG_ALPHA_LARGE_VMALLOC
    memcpy (ret + USER_PTRS_PER_PGD, init + USER_PTRS_PER_PGD,
    (PTRS_PER_PGD – USER_PTRS_PER_PGD – 1)*sizeof(pgd_t));
    #else
    pgd_val(ret[PTRS_PER_PGD-2]) = pgd_val(init[PTRS_PER_PGD-2]);
    #endif
    /* The last PGD entry is the VPTB self-map. */
    pgd_val(ret[PTRS_PER_PGD-1])
    = pte_val(mk_pte(virt_to_page(ret), PAGE_KERNEL));
    }
    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;
    }
    struct mm_struct {
    struct vm_area_struct * mmap; /* list of VMAs */
    struct rb_root mm_rb;
    struct vm_area_struct * mmap_cache; /* last find_vma result */
    unsigned long (*get_unmapped_area) (struct file *filp,
    unsigned long addr, unsigned long len,
    unsigned long pgoff, unsigned long flags);
    void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
    unsigned long mmap_base; /* base of mmap area */
    unsigned long task_size; /* size of task vm space */
    unsigned long cached_hole_size; /* if non-zero, the largest hole
    below free_area_cache */
    unsigned long free_area_cache; /* first hole of size
    cached_hole_size or larger */
    pgd_t * pgd; // ⻚⽬录起始地址
    }

    2.4 可以进⾏任意参数传递的线程封装demo

    #include <iostream> #include <functional> // ⽤于 std::function 和 std::bind #include <memory> // ⽤于 std::shared_ptr 和 std::unique_ptr #include <pthread.h> // POSIX 线程库 #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::function<void()> 对象 // 使⽤ std::bind 将函数和参数绑定在⼀起 // std::forward ⽤于完美转发参数,保持参数的左值/右值属性 auto task = std::make_shared<std::function<void()>>(
    std::bind(std::forward<Callable>(func), std::forward<Args>
    (args)…)
    );
    // 将任务传递给线程⼊⼝函数 // 使⽤ new 在堆上分配 std::shared_ptr,确保任务对象在线程执⾏期间有效 if (pthread_create(&thread_id_, nullptr, &Thread::threadEntry, new
    std::shared_ptr<std::function<void()>>(task)) != 0)
    {
    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_; // 线程 ID bool running_; // 线程是否在运⾏ // 线程⼊⼝函数 static void* threadEntry(void* arg) {
    // 从参数中提取任务并执⾏ // 使⽤ std::unique_ptr 管理 std::shared_ptr 的指针,确保资源释放
    std::unique_ptr<std::shared_ptr<std::function<void()>>> task_ptr(
    static_cast<std::shared_ptr<std::function<void()>> *>(arg));
    auto task = *task_ptr; // 解引⽤获取任务对象
    (*task)(); // 执⾏任务 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;
    // 启动线程,传递任意函数和参数 // 这⾥传递了⼀个普通函数 printMessage 和两个参数 "Hello, World!" 和 42
    thread.start(printMessage, "Hello, World!", 42, 1, 2, 3);
    // 等待线程结束
    thread.join();
    return 0;
    }

    $ ./a.out
    Message: Hello, World!, Value: 42
    a:1
    b:2
    c:3

    $ ps -aL
    PID LWP TTY TIME CMD
    923509 923509 pts/1 00:00:00 a.out
    923509 923510 pts/1 00:00:00 a.out

    2.5 我们⾃⼰调⽤⼀下clone

    • 直接AI形成

    #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 == NULL)
    {
    perror("malloc");
    exit(EXIT_FAILURE);
    }

    // 使⽤ clone 创建⼦进程 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;

    线程封装不仅是将系统调用包装为类或函数,更是对并发执行模型的一种抽象。从用户层的 Thread 类,到 glibc 的 pthread_create,再到内核的 clone 与 task_struct,整条链路体现了一个清晰的“分层抽象”思想:上层关注逻辑与易用性,下层关注效率与资源管理。

    理解这一链路,不仅能帮助我们写出更健壮的多线程程序,也能为后续学习协程、异步 I/O、用户态调度等高级并发模型奠定坚实的基础。正如文中所说:“真到了哪一步,就直接用 C++11 吧”,但在此之前,深入理解系统底层的实现逻辑,仍是每一位系统程序员必备的修炼。

    赞(0)
    未经允许不得转载:171主机测评 » C++线程封装与实现详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址