Page 3 of 3

Re: Troubles implementing multitasking

Posted: Sat Aug 18, 2018 5:12 am
by frabert
@Brendan I'm not sure if this should be addressed here or in the wiki's discussion page, I'll move the post if necessary.
The current issue I'm facing while following your tutorial is this: let's suppose I have a task A, which blocks because it needs to wait for B to finish.

Looking at the code for block_task and unblock_task:

Code: Select all

void block_task(int reason) {
    lock_scheduler();
    current_task_TCB->state = reason;
    schedule();
    unlock_scheduler();
}

void unblock_task(thread_control_block * task) {
    lock_scheduler();
    if(first_ready_to_run_task == NULL) {
        // Only one task was running before, so pre-empt
        switch_to_task(task);
    } else {
        // There's at least one task on the "ready to run" queue already, so don't pre-empt
        last_ready_to_run_task->next = task;
        last_ready_to_run_task = task;
    }
    unlock_scheduler();
}
this happens:

Code: Select all

// A is running, sched_lock_cnt=0:
lock_scheduler(); // sched_lock=1, interrupts disabled
schedule(); // Now B is running
// Something something...
// B ends, unblocks A
lock_scheduler(); // sched_lock_cnt=2
switch_tasks(); // Now A is running
unlock_scheduler(); // sched_lock_cnt=1
// A continues its way, never to be interrupted again...

Re: Troubles implementing multitasking

Posted: Sat Aug 18, 2018 3:49 pm
by Brendan
frabert wrote:@Brendan I'm not sure if this should be addressed here or in the wiki's discussion page, I'll move the post if necessary.
The current issue I'm facing while following your tutorial is this: let's suppose I have a task A, which blocks because it needs to wait for B to finish.

Looking at the code for block_task and unblock_task:

Code: Select all

void block_task(int reason) {
    lock_scheduler();
    current_task_TCB->state = reason;
    schedule();
    unlock_scheduler();
}

void unblock_task(thread_control_block * task) {
    lock_scheduler();
    if(first_ready_to_run_task == NULL) {
        // Only one task was running before, so pre-empt
        switch_to_task(task);
    } else {
        // There's at least one task on the "ready to run" queue already, so don't pre-empt
        last_ready_to_run_task->next = task;
        last_ready_to_run_task = task;
    }
    unlock_scheduler();
}
this happens:

Code: Select all

// A is running, sched_lock_cnt=0:
lock_scheduler(); // sched_lock=1, interrupts disabled
schedule(); // Now B is running
// Something something...
// B ends, unblocks A
lock_scheduler(); // sched_lock_cnt=2
switch_tasks(); // Now A is running
unlock_scheduler(); // sched_lock_cnt=1
// A continues its way, never to be interrupted again...
There's a paragraph near the end of "Step 5: Race Conditions and Locking Version 1" that tries to explain this:

Note that this can be a little confusing depending on your perspective. From the perspective of one task, it locks the scheduler, does a task switch, and then when it gets CPU time again it unlocks the scheduler; and this is fine (the scheduler is always unlocked after its locked). From the perspective of the CPU, one task locks the scheduler then the scheduler does a task switch and a completely different task unlocks the scheduler; and this is also fine (the scheduler is still unlocked after its locked).

In other words; this happens:

Code: Select all

// A is running, sched_lock_cnt=0:
lock_scheduler(); // sched_lock=1, interrupts disabled
schedule(); // Now B is running
unlock_scheduler(); // B unlocks the scheduler, sched_lock_cnt=0

// Something something...

lock_scheduler(); // B locks the scheduler, sched_lock_cnt=1
switch_tasks(); // Now A is running
unlock_scheduler(); // A unlocks the scheduler, sched_lock_cnt=0

// A continues its way
Note that this is the same issue you were having before you read the tutorial. This makes me wonder if the tutorial is adequate for other people (who don't have pre-existing expectations of a problem), or if the tutorial needs to explain it better (either because people without pre-existing expectations will also think there's a problem or because other people are likely to have pre-existing expectations).


Cheers,

Brendan

Re: Troubles implementing multitasking

Posted: Sun Aug 19, 2018 2:30 am
by frabert
That makes sense if B was being resumed from being blocked, so that it was put to sleep by calling schedule, so that when it awakens it returns from schedule and unlocks the scheduler. But what if B was being started for the first time? In that case, B would not start its execution by returning from a schedule call, so no scheduler unlocking would be done, so I presume I need to add a "preamble" to all the tasks to unlock the scheduler when they are first started?

Re: Troubles implementing multitasking

Posted: Sun Aug 19, 2018 4:37 pm
by Brendan
Hi,
frabert wrote:That makes sense if B was being resumed from being blocked, so that it was put to sleep by calling schedule, so that when it awakens it returns from schedule and unlocks the scheduler. But what if B was being started for the first time? In that case, B would not start its execution by returning from a schedule call, so no scheduler unlocking would be done, so I presume I need to add a "preamble" to all the tasks to unlock the scheduler when they are first started?
Ah - I understand now. Yes; when a task first starts running it'd need to unlock the scheduler as one of the first things it does (and if it doesn't do this the scheduler will remain locked and it'll cause problems).

Also, the kernel might want to pre-initialise some things (e.g. setting any "caller saved" registers that the task switch code doesn't load to known values, possibly including FPU/MMX/SSE/AVX state later on) or do some other housekeeping (I don't know what - maybe change some flag so that people can tell the difference between "not started yet" and "started properly"?). If you want to avoid having "preamble" in the code for every kernel task (and want a nice place to add/change "initialisation for every kernel task"); you can have a generic "new task startup routine" that releases scheduler's lock and does any other housekeeping/preperation before passing control to task specific code. In this case, when creating the new task's kernel stack your "create_kernel_task()" function would set the "return EIP" (used for the first task switch) to the address of the startup routine and then put the caller's "address that the startup function should return to" on the kernel stack too (plus any other parameters - e.g. if your "create_kernel_task()" function has a generic "void * arg" parameter like "pthread_create()" does, then that would be put on the new kernel stack too).

The tutorial doesn't mention any of this - I'll try to fix that a little later today. :)


Thanks,

Brendan