Hi guys,
A little bit of preamble will help me segue into the topic at hand. I've been using chatGPT quite often over the past 4-6 weeks. Although not always 100% accurate, it's still a great tool and has definitely augmented my programming experience. I have found it explains concept topics nicely. With that being said; I emphasise "not always 100% accurate" as I have a hunch that it's not telling me the correct execution flow, of alternatively, I'm mistaken.
This is quite a complex subject, and I'm probably out of my depth here. But I still want to jump into the deep end. So I asked chatGPT to create code to simulate threading, and what a (very) simple threading library would potentially look like(just a rough and ready example, not production code).
What better way to learn about user-level threading than delving deep into the implementation details(as crude as it may be). Below is the code, if anyone could please fill in my gaps, this would be more than appreciated. I believe that I'm failing to understand the execution flow of the program and what longjmp and setjmp are really doing.
So I'll begin; setjmp() is used to save the current execution context(CPU registers, stack pointer, etc), we pass a jmp_buf as the argument to this, jmp_buf is a buffer that saves the context. When we call longjmp(), we pass in the context and we basically load the context from our jmp_buf and continue execution from that context. When we first call setjmp() it returns 0, indicating that we have not jumped yet. When we call longjmp() we jump back to where setjmp() left off, this time setjmp() returns 1 indicating that we have jumped back to that execution context.
So now for the code(note this code was written by chatGPT and not me);
1) we declare two threads
2) then call create_thread(&thread1, example_thread_function) for the first thread
2 i) we point our start routine to the routine that we specify as args
2 ii) we create memory on the heap which will be used for our stack
2 iii) call setjmp() and pass it our thread1s context buffer(stores context)
2 iv) setjmp() returns 0 and if block is executed
2 v) set the stack pointer to point to our dynamically allocated stack
2 vi) callq calls our function passed as arg,will put ret addr on thread1s stack
2 vii) we are now in the function example_thread_function
2 viii) we loop 5 times
2 ix) i = 0, i < 4: so we call yield_thread(&thread1,&thread2)
2 x) **this is where my understanding breaks down(below)...
2 x) we are now in yield_thread(): we call setjmp(current->context)...
2 x) which is thread1->context
So I'm assuming my understanding of the execution flow is largely correct up until "2 x". So what happens here? Since current equals thread1, we save the current context in threads 1 context buffer, but now we have just overwritten this buffer which contained the previous context just before we called the example_thread_function() in create_thread(), why have we done this and what's the point of saving our context in create_thread() if we just overwrite it in yield_thread()?
Next since setjmp() returns 0, we execute longjmp(next->context,1) but where are we jumping to? next = thread 2, yet we haven't gotten to the line of code to actually create the second thread, thus I thought thread2s context buffer hasn't been set yet, so again, where are we jumping to?
Thanks
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
|
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#define STACK_SIZE 4096 // define stack size on the heap
typedef struct {
jmp_buf context;
void (*start_routine)(void);
void *stack;
} thread_t;
void create_thread(thread_t *thread, void (*start_routine)(void)) {
thread->start_routine = start_routine;
thread->stack = malloc(STACK_SIZE);
if (thread->stack == NULL) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
if (setjmp(thread->context) == 0) {
// init the stack pointer and jump to the start routine
__asm__ volatile (
"movq %[stack], %%rsp;"
"callq *%[start_routine];"
:
: [stack] "r" (thread->stack + STACK_SIZE),
[start_routine] "r" (start_routine)
: "memory"
);
}
}
void yield_thread(thread_t *current, thread_t *next) {
// Save the current context and jump to the next context
if (setjmp(current->context) == 0) {
longjmp(next->context, 1);
}
}
void join_thread(thread_t *thread) {
free(thread->stack);
}
void example_thread_function() {
for (int i = 0; i < 5; ++i) {
printf("Thread: %d\n", i);
// simulate thread yielding to another thread
if (i < 4) {
yield_thread(&thread1, &thread2);
}
}
}
thread_t thread1, thread2;
int main() {
// creation of two new threads
create_thread(&thread1, example_thread_function);
create_thread(&thread2, example_thread_function);
// schedule the threads
for (int i = 0; i < 5; ++i) {
yield_thread(&thread1, &thread2);
yield_thread(&thread2, &thread1);
}
// join threads
join_thread(&thread1);
join_thread(&thread2);
return 0;
}
|