Unikraft pthread-embedded源码分析

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
lib-pthread-embedded/
├── attributes.c
├── CODING_STYLE.md
├── Config.uk
├── CONTRIBUTING.md
├── COPYING.md
├── include
│   ├── pte_osal.h
│   ├── pte_types.h
│   ├── pthread.h
│   ├── sched.h
│   └── sys
│   └── _pthreadtypes.h
├── MAINTAINERS.md
├── Makefile.uk
├── patches
│   ├── 0001-tests-bugfix-Set-type-size-of-result-used-in-pthread.patch
│   ├── 0002-tests-bugfix-Pass-right-arguments-on-tests-exit4-and.patch
│   ├── 0003-tests-bugfix-Fix-macro-definitions-for-tests-once3-a.patch
│   ├── 0004-tests-Disable-priority-tests-until-we-add-preemptive.patch
│   ├── 0005-Use-Unikraft-yield-function.patch
│   ├── 0006-Separate-header-inclusions-for-functions-from-header.patch
│   ├── 0007-Use-atomic-operations-as-macros.patch
│   ├── 0008-bugfix-Fix-atomic-operations-on-semaphore.patch
│   ├── 0009-Make-pte_handle-a-C-structure.patch
│   ├── 0010-Added-operator-for-pte_handle_t.patch
│   ├── 0011-Add-clockid_t-field-to-pthread_condattr_t_.patch
│   └── 0012-Add_pthread_kill_when_uk_signals_are_enabled.patch
├── pte_osal.c
├── pthread_atfork.c
├── pthread_condattr.c
├── pthread_sigmask.c
└── README.md

结构

主要引用unikraft中的uksched、uklock、uksignal几个库在封装,形成了pthread-embedded。
其中可以看到几个头文件与uksched中的同名,例如sched.h,pthread.h等。解决同名问题,遂采用了#include_next<>来引入头文件。

但看pthread-embedded和unikraft的工程文件,会发现pthread-embedded中很多函数都找不到声明。此时查看makefile.uk文件可以看到,似乎是引入了RWTH-OS/pthread-embedded
中的一些文件。

因此pthread-embedded是基于unikraft提供的一些接口以及RWTH-OS/pthread-embedded这两个库实现的一个线程库。

数据结构

这部分介绍了pthread-embedded中比较重要的数据结构。其中pte_thread_data的数据注释是比较清晰的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef struct pte_thread_data {

    /* thread routine */

    pte_osThreadEntryPoint entry_point;

    /* thread routine arguments */

    void *argv;

    /* Unikraft thread */

    struct uk_thread *uk_thread;

    /* TLS */

    void *tls;

    /* Semaphore for triggering thread start */

    struct uk_semaphore start_sem;

    /* Semaphore for cancellation */

    struct uk_semaphore cancel_sem;

    /* Is non-zero if thread exited */

    int done;

} pte_thread_data_t;
  • entry_point : 函数的入口,线程主体运行的入口。
  • argv : 传递给entry_point的参数
  • uk_thread : unikraft中与之11对应的线程
  • tls : 线程局部存储
  • start_sem : 线程启动的信号量
  • cancel_sem : 线程取消的信号量
  • done : 标记线程状态,非0代表线程为退出态
1
2
3
4
5
6
7
8
9
static const void *PTE_CAPSULE_MAGIC = &PTE_CAPSULE_MAGIC;

struct pte_entry_capsule {

    pte_osThreadEntryPoint entry_point;

    void *argv;

};

PTE_CAPSULE_MAGIC是一个指向自身的Magic Number。
&PTE_CAPSULE_MAGIC是一个指向PTE_CAPSULE_MAGIC的指针,是一个内存地址。因此这是一个指向自身的指针。
pet_entry_capsule主要用于封装程序入口,以及要传递的参数。
以上具体的作用将在后面提到。

1
2
3
4
5
typedef struct pte_thread_data *pte_osThreadHandle;

typedef struct uk_semaphore *pte_osSemaphoreHandle;

typedef struct uk_mutex *pte_osMutexHandle;

这三个作为处理器,在thread-embedded中一般作为参数传递使用,且传进去以后也强制类型转化成原类型。

初始化

pte_osInit主要对一些全局的变量进行声明,其中的一些函数在RWTH-OS/pthread-embedded中被实现,并且通过makefile链接进来。查阅后发现主要是对锁、内存等变量的声明、分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
pte_osResult pte_osInit(void)

{

    pte_osResult result = PTE_OS_OK;

    pte_thread_data_t *ptd;

    struct uk_thread *crnt;



    /* Allocate and initialize TLS support */

    result = pteTlsGlobalInit(CONFIG_LIBPTHREAD_EMBEDDED_MAX_TLS);

    if (result != PTE_OS_OK) {

        uk_pr_err("Could not init global TLS");

        goto out;

    }



    /* Create a ptd for initializing thread. */

    ptd = calloc(1, sizeof(pte_thread_data_t));

    if (ptd == NULL) {

        result = PTE_OS_NO_RESOURCES;

        goto out;

    }



    ptd->tls = pteTlsThreadInit();

    if (ptd->tls == NULL) {

        uk_pr_err("Could not init TLS");

        free(ptd);

        result = PTE_OS_NO_RESOURCES;

        goto out;

    }



    crnt = uk_thread_current();

    crnt->prv = ptd;

    ptd->uk_thread = crnt;



out:

    return result;

}

信号处理

1
2
3
4
5
6
7
8
9
10
11
#if CONFIG_LIBUKSIGNAL

int pte_kill(pte_osThreadHandle threadId, int sig)

{

    return uk_sig_thread_kill(threadId->uk_thread, sig);

}

#endif

kill在linux中默认用作终止进程,但是也可以用作向某一进程发送信号量。在pthread-embedded中直接调用了uksignal中的接口。在这pthread-embedded非常常见,可以说pthread就是基于unikraft提供的接口才实现的。

线程

线程创建

pte_osThreadCreate调用了Unikraft中的uksched API创建了线程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pte_osResult pte_osThreadCreate(pte_osThreadEntryPoint entry_point,

    int stack_size, int initial_prio, void *argv,

    pte_osThreadHandle *ph)

{

    struct pte_entry_capsule capsule;

    struct uk_thread *th;



    capsule.entry_point = entry_point;

    capsule.argv        = argv;



    /* Create the Unikraft thread. This will cause that

     * pte_osInitThread() is called.

     */

    th = uk_thread_create_attr(NULL, NULL,

                   PTE_CAPSULE_MAGIC, &capsule);

    if (!th)

        return PTE_OS_NO_RESOURCES;



    /* pte_osInitThread() should have setup a newly created

     * pte_thread_data_t which should be stored on th->prv

     */

    UK_ASSERT(th->prv != NULL);



    /* Return the thread handle */

    *ph = th->prv;

    return PTE_OS_OK;

}

其中将函数的入口(entry_point)和函数参数(argv)使用结构体pte_entry_capsule封装,该结构体在创建线程时,与unikraft中的uk_thread的参数arg进行绑定。而pte_osThreadHandle则是uk_thread中的prv参数。
除了传入pte_entry_capsule,还传入了PTE_CAPSULE_MAGIC。此时,如果是通过pte_osThreadCreate函数创建的线程,那么此时在uk_thread中的func参数与之对应的是PTE_CAPSULE_MAGIC,也就是函数自身。
回到uk_thread_create_attr这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
#define uk_thread_create_attr(name, attr, function, data) \

    uk_sched_thread_create(uk_sched_get_default(), \

            name, attr, function, data)
         

struct uk_thread *uk_sched_thread_create(struct uk_sched *sched,

        const char *name, const uk_thread_attr_t *attr,

        void (*function)(void *), void *arg)

{

    // start:申请空间

    struct uk_thread *thread = NULL;

    void *stack = NULL;

    int rc;

    void *tls = NULL;



    thread = uk_malloc(sched->allocator, sizeof(struct uk_thread));

    if (thread == NULL) {

        uk_pr_err("Failed to allocate thread\n");

        goto err;

    }



    /* We can't use lazy allocation here

     * since the trap handler runs on the stack

     */

    stack = create_stack(sched->allocator);

    if (stack == NULL)

        goto err;

    if (have_tls_area() && !(tls = uk_thread_tls_create(sched->allocator)))

        goto err;

    // end 申请

    // 先create thread 再 init thread

    rc = uk_thread_init(thread,

            &sched->plat_ctx_cbs, sched->allocator,

            name, stack, tls, function, arg);

    if (rc)

        goto err;



    rc = uk_sched_thread_add(sched, thread, attr);

    if (rc)

        goto err_add;



    return thread;



err_add:

    uk_thread_fini(thread, sched->allocator);

err:

    if (tls)

        uk_free(sched->allocator, tls);

    if (stack)

        uk_free(sched->allocator, stack);

    if (thread)

        uk_free(sched->allocator, thread);



    return NULL;

}

int uk_thread_init(struct uk_thread *thread,

        struct ukplat_ctx_callbacks *cbs, struct uk_alloc *allocator,

        const char *name, void *stack, void *tls,

        void (*function)(void *), void *arg)

{

    unsigned long sp;

    void *ctx;

    int ret = 0;

    struct uk_thread_inittab_entry *itr;



    UK_ASSERT(thread != NULL);

    UK_ASSERT(stack != NULL);

    UK_ASSERT(!have_tls_area() || tls != NULL);



    /* Save pointer to the thread on the stack to get current thread */

    *((unsigned long *) stack) = (unsigned long) thread;



    /* Allocate thread context */

    ctx = uk_zalloc(allocator, ukplat_thread_ctx_size(cbs));

    if (!ctx) {

        ret = -1;

        goto err_out;

    }



    memset(thread, 0, sizeof(*thread));

    thread->ctx = ctx;

    thread->name = name;

    thread->stack = stack;

    thread->tls = tls;

    thread->entry = function;

    thread->arg = arg;



    /* Not runnable, not exited, not sleeping */

    thread->flags = 0;

    thread->wakeup_time = 0LL;

    thread->detached = false;

    uk_waitq_init(&thread->waiting_threads);

    thread->sched = NULL;

    thread->prv = NULL;



    /* TODO: Move newlibc reent initialization to newlib as

     *       thread initialization function

     */

#ifdef CONFIG_LIBNEWLIBC

    reent_init(&thread->reent);

#endif



    /* Iterate over registered thread initialization functions */

    uk_thread_inittab_foreach(itr) {

        if (unlikely(!itr->init))

            continue;



        uk_pr_debug("New thread %p: Call thread initialization function %p...\n",

                thread, *itr->init);

        ret = (itr->init)(thread);

        if (ret < 0)

            goto err_fini;

    }



    /* Prepare stack and TLS

     * NOTE: In case the function pointer was changed by a thread init

     *       function (e.g., encapsulation), we prepare the stack here

     *       with the final setup

     */

    init_sp(&sp, stack, thread->entry, thread->arg);



    /* Platform specific context initialization */

    ukplat_thread_ctx_init(cbs, thread->ctx, sp,

                   (uintptr_t) ukarch_tls_pointer(tls));



    uk_pr_info("Thread \"%s\": pointer: %p, stack: %p, tls: %p\n",

           name, thread, thread->stack, thread->tls);



    return 0;



err_fini:

    /* Run fini functions starting from one level before the failed one

     * because we expect that the failed one cleaned up.

     */

    uk_thread_inittab_foreach_reverse2(itr, itr - 2) {

        if (unlikely(!itr->fini))

            continue;

        (itr->fini)(thread);

    }

    uk_free(allocator, thread->ctx);

err_out:

    return ret;

}

两个线程结构体的参数间的对应关系如下表(可能存在错误)所示。

pthread-embedded 在函数中传递时 uksched
argv 不确定是否被封装进capsule中
entry_point 不确定是否被封装进capsule中
capsule data arg
ptd结构体 prv
uk_thread uk_thread结构体
PTE_CAPSULE_MAGIC function entry

线程启动

申请线程启动信号量,以下函数应该是作为通用的线程函数入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void uk_stub_thread_entry(void *argv)

{

    pte_thread_data_t *ptd = (pte_thread_data_t *) argv;



    /* wait for the resume command */

    uk_semaphore_down(&ptd->start_sem);



    ptd->entry_point(ptd->argv);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// 通过uksched API 创建一个线程。

// 通过此方法创建的线程 func是自己 arg是capsule

pte_osResult pte_osThreadCreate(pte_osThreadEntryPoint entry_point,

    int stack_size, int initial_prio, void *argv,

    pte_osThreadHandle *ph)

{

    struct pte_entry_capsule capsule;

    struct uk_thread *th;



    capsule.entry_point = entry_point;

    capsule.argv        = argv;



    /* Create the Unikraft thread. This will cause that

     * pte_osInitThread() is called.

     */

    th = uk_thread_create_attr(NULL, NULL,

                   PTE_CAPSULE_MAGIC, &capsule);

    if (!th)

        return PTE_OS_NO_RESOURCES;

    // 调用完以上后 th->prv应该是NULL

    // 感觉少了一步
// 根据注释

    /* Create the Unikraft thread. This will cause that

     * pte_osInitThread() is called.

     */
// 应是调用完后会自行调用pte_osInitThread


    /* pte_osInitThread() should have setup a newly created

     * pte_thread_data_t which should be stored on th->prv

     */

    UK_ASSERT(th->prv != NULL);



    // TODO:th->prv是什么时候存入的?

    /* Return the thread handle */

    *ph = th->prv;

    return PTE_OS_OK;

}



static int pte_osInitThread(struct uk_thread *th)

{

    pte_thread_data_t *ptd;

    struct pte_entry_capsule *capsule;



    /* NOTE: We reserve th->prv for our exclusive use,

     *       so it should be NULL when entering here

     */

    UK_ASSERT(th->prv == NULL);



    /* Initialize pte with first thread creation */

    if (unlikely(!initialized)) {

        uk_pr_warn("Thread %p created without " STRINGIFY(__LIBNAME__)

               " initialized. Utilizing the pthread API from this context may lead to memory leaks.\n",

               th);

        return 0;

    }



    ptd = calloc(1, sizeof(pte_thread_data_t));

    if (!ptd)

        goto err_out;



    /* Allocate TLS structure for this thread. */

    ptd->tls = pteTlsThreadInit();

    if (ptd->tls == NULL) {

        uk_pr_err("Could not allocate TLS\n");

        goto err_free_ptd;

    }



    /* How did we enter this function? */

    /* 区分线程是通过哪个API创建的 */

    if (th->entry == PTE_CAPSULE_MAGIC) {

        /* This thread got created by pte_osThreadCreate()!

         * Lets have a look into the capsule.

         */

        UK_ASSERT(th->arg);



        capsule = (struct pte_entry_capsule *) th->arg;



        ptd->entry_point = capsule->entry_point;

        ptd->argv        = capsule->argv;



        /* this thread has to wait for further setup */

        uk_semaphore_init(&ptd->start_sem, 0);

    } else {

        /* We will encapsulate our thread entry point,

         * we have to move our actual entry to ptd

         */

        ptd->entry_point = (pte_osThreadEntryPoint) th->entry;

        ptd->argv        = th->arg;



        /* uksched threads need to start automatically */

        uk_semaphore_init(&ptd->start_sem, 1);

    }



    /* Setup encapsulated entry point */

    th->entry = uk_stub_thread_entry;

    th->arg   = ptd;

    uk_semaphore_init(&ptd->cancel_sem, 0);

    ptd->done = 0;



    /* Store cross references (uk_thread <-> pte_thread_data_t) */

    th->prv = ptd;

    ptd->uk_thread = th;



#if CONFIG_LIBUKSIGNAL

    /* inherit signal mask */

    ptd->uk_thread->signals_container.mask =

        uk_thread_current()->signals_container.mask;

#endif

    return 0;



err_free_ptd:

    free(ptd);

err_out:

    return -1;

}

在前面我们知道,如果是通过pte_osThreadCreate创建的函数,此时func应是PTE_CAPSULE_MAGIC;
arg是前面封装好的capsule。如果不是通过pte_osThreadCreate创建的函数(这里没看到具体的常见,猜测应是系统内部自带的线程,也需要统一纳入到pthread-embedded进行管理),则不是这样。因此就产生了以下分歧。

在uk_thread中的参数说明 不通过uksched API创建的线程 通过uksched API创建的线程
prv pte_osThreadEntryPoint pte_osThreadEntryPoint
arg 应传入函数的参数 pte_entry_capsule
entry 函数入口 无实际意义

以上函数则希望完成对二者的兼容,统一纳入管理。
函数入口的统一模板如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void uk_stub_thread_entry(void *argv)

{

    pte_thread_data_t *ptd = (pte_thread_data_t *) argv;



    /* wait for the resume command */

    uk_semaphore_down(&ptd->start_sem);



    ptd->entry_point(ptd->argv);

}

对于通过uksched API创建的线程

主要就是拿到pte_entry_capsule封装的函数指针和函数参数,从uk_thread中放到pte中,同时将uk_thread中的函数入口改为uk_stub_thread_entry。

对于不通过uksched API创建的线程

直接取到uk_thread中的函数入口指针和参数,装入pte中,同时将uk_thread中的函数入口改为uk_stub_thread_entry。
二者的主要区别就是通过不同方式创建的线程,在内核中uk_thread中一些属性代表的值会有所区别。

锁与信号量

与kill函数类似,这部分就是对unikraft相关接口的再封装。这部分的相关函数,同质化比较严重,也比较简单,就不展开细讲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pte_osResult pte_osMutexLock(pte_osMutexHandle h)

{

    if (!h)

        return PTE_OS_INVALID_PARAM;



    uk_mutex_lock(h);



    return PTE_OS_OK;

}

总结

pthread-embedded是一个非常简洁的线程实现。它主要基于unikraft提供的接口和一个来自github上的库,构建了一套属于自己的线程管理逻辑。这种设计方式,和linux内核中的nptl之于pthread的关系非常相似。