[kernel] Add thead kobject class
Add the thread kernel API object and move the scheduler to use threads instead of processes for scheduling and task switching.
This commit is contained in:
@@ -2,17 +2,25 @@
|
||||
/// \file signals.h
|
||||
/// Collection of constants for the j6_signal_t type
|
||||
|
||||
// Signals 0-7 are common to all types
|
||||
#define j6_signal_no_handles (1 << 0)
|
||||
// Signals 0-15 are common to all types
|
||||
#define j6_signal_no_handles (1ull << 0)
|
||||
|
||||
// Signals 8-15 are user-defined signals
|
||||
#define j6_signal_user0 (1 << 8)
|
||||
#define j6_signal_user1 (1 << 9)
|
||||
#define j6_signal_user2 (1 << 10)
|
||||
#define j6_signal_user3 (1 << 11)
|
||||
#define j6_signal_user4 (1 << 12)
|
||||
#define j6_signal_user5 (1 << 13)
|
||||
#define j6_signal_user6 (1 << 14)
|
||||
#define j6_signal_user7 (1 << 15)
|
||||
// Signals 16-47 are defined per-object-type
|
||||
|
||||
// All other signals are type-specific
|
||||
// Signals 48-63 are user-defined signals
|
||||
#define j6_signal_user0 (1ull << 48)
|
||||
#define j6_signal_user1 (1ull << 49)
|
||||
#define j6_signal_user2 (1ull << 50)
|
||||
#define j6_signal_user3 (1ull << 51)
|
||||
#define j6_signal_user4 (1ull << 52)
|
||||
#define j6_signal_user5 (1ull << 53)
|
||||
#define j6_signal_user6 (1ull << 54)
|
||||
#define j6_signal_user7 (1ull << 55)
|
||||
#define j6_signal_user8 (1ull << 56)
|
||||
#define j6_signal_user9 (1ull << 57)
|
||||
#define j6_signal_user10 (1ull << 58)
|
||||
#define j6_signal_user11 (1ull << 59)
|
||||
#define j6_signal_user12 (1ull << 60)
|
||||
#define j6_signal_user13 (1ull << 61)
|
||||
#define j6_signal_user14 (1ull << 62)
|
||||
#define j6_signal_user15 (1ull << 63)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct process;
|
||||
struct TCB;
|
||||
|
||||
struct cpu_state
|
||||
{
|
||||
@@ -18,7 +18,7 @@ struct cpu_data
|
||||
{
|
||||
uintptr_t rsp0;
|
||||
uintptr_t rsp3;
|
||||
process *tcb;
|
||||
TCB *tcb;
|
||||
};
|
||||
|
||||
extern cpu_data bsp_cpu_data;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "interrupts.h"
|
||||
#include "io.h"
|
||||
#include "log.h"
|
||||
#include "page_manager.h"
|
||||
#include "scheduler.h"
|
||||
#include "syscall.h"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ ramdisk_process_loader:
|
||||
|
||||
pop rdi ; the address of the program image
|
||||
pop rsi ; the size of the program image
|
||||
pop rdx ; the address of this process' process structure
|
||||
pop rdx ; the address of this thread's TCB
|
||||
|
||||
call load_process_image
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ void memory_initialize_post_ctors(kernel::args::header *kargs);
|
||||
|
||||
using namespace kernel;
|
||||
|
||||
/*
|
||||
class test_observer :
|
||||
public kobject::observer
|
||||
{
|
||||
@@ -67,6 +68,7 @@ public:
|
||||
|
||||
const char *m_name;
|
||||
};
|
||||
*/
|
||||
|
||||
void
|
||||
init_console()
|
||||
@@ -174,7 +176,7 @@ kernel_main(args::header *header)
|
||||
syscall_enable();
|
||||
scheduler *sched = new (&scheduler::get()) scheduler(devices.get_lapic());
|
||||
|
||||
sched->create_kernel_task(-1, logger_task, scheduler::max_priority-1, process_flags::const_pri);
|
||||
sched->create_kernel_task(logger_task, scheduler::max_priority-1, true);
|
||||
|
||||
for (auto &ird : initrds) {
|
||||
for (auto &f : ird.files()) {
|
||||
@@ -185,6 +187,7 @@ kernel_main(args::header *header)
|
||||
|
||||
log::info(logs::objs, "Testing object system:");
|
||||
|
||||
/*
|
||||
test_observer obs1("event");
|
||||
test_observer obs2("no handles");
|
||||
{
|
||||
@@ -197,6 +200,7 @@ kernel_main(args::header *header)
|
||||
|
||||
handle h(1, 0, &e);
|
||||
}
|
||||
*/
|
||||
|
||||
sched->start();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "j6/signals.h"
|
||||
#include "j6/types.h"
|
||||
#include "objects/kobject.h"
|
||||
#include "objects/thread.h"
|
||||
|
||||
// TODO: per-cpu this?
|
||||
static j6_koid_t next_koid;
|
||||
@@ -14,7 +15,8 @@ kobject::kobject(type t, j6_signal_t signals) :
|
||||
|
||||
kobject::~kobject()
|
||||
{
|
||||
notify_signal_observers(0, j6_status_destroyed);
|
||||
for (auto *t : m_blocked_threads)
|
||||
t->wake_on_result(this, j6_status_destroyed);
|
||||
}
|
||||
|
||||
j6_koid_t
|
||||
@@ -33,58 +35,30 @@ void
|
||||
kobject::assert_signal(j6_signal_t s)
|
||||
{
|
||||
m_signals |= s;
|
||||
notify_signal_observers(s);
|
||||
notify_signal_observers();
|
||||
}
|
||||
|
||||
void
|
||||
kobject::deassert_signal(j6_signal_t s)
|
||||
{
|
||||
m_signals &= ~s;
|
||||
notify_signal_observers(s);
|
||||
}
|
||||
|
||||
void
|
||||
kobject::register_signal_observer(observer *object, j6_signal_t s)
|
||||
kobject::notify_signal_observers()
|
||||
{
|
||||
m_observers.emplace(object, s);
|
||||
}
|
||||
size_t i = 0;
|
||||
bool readied = false;
|
||||
while (i < m_blocked_threads.count()) {
|
||||
thread *t = m_blocked_threads[i];
|
||||
|
||||
void
|
||||
kobject::deregister_signal_observer(observer *object)
|
||||
{
|
||||
for (size_t i = 0; i < m_observers.count(); ++i) {
|
||||
auto ® = m_observers[i];
|
||||
if (reg.object != object) continue;
|
||||
|
||||
reg = m_observers[m_observers.count() - 1];
|
||||
m_observers.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
kobject::notify_signal_observers(j6_signal_t mask, j6_status_t result)
|
||||
{
|
||||
for (auto ® : m_observers) {
|
||||
if (mask == 0 || (reg.signals & mask) != 0) {
|
||||
if (!reg.object->on_signals_changed(this, m_signals, mask, result))
|
||||
reg.object = nullptr;
|
||||
if (t->wake_on_signals(this, m_signals)) {
|
||||
m_blocked_threads.remove_swap_at(i);
|
||||
readied = true;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// Compact the observer list
|
||||
long last = m_observers.count() - 1;
|
||||
while (m_observers[last].object == nullptr && last >= 0) last--;
|
||||
|
||||
for (long i = 0; i < long(m_observers.count()) && i < last; ++i) {
|
||||
auto ® = m_observers[i];
|
||||
if (reg.object != nullptr) continue;
|
||||
reg = m_observers[last--];
|
||||
|
||||
while (m_observers[last].object == nullptr && last >= i) last--;
|
||||
}
|
||||
|
||||
m_observers.set_size(last + 1);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "j6/types.h"
|
||||
#include "kutil/vector.h"
|
||||
|
||||
class thread;
|
||||
|
||||
/// Base type for all user-interactable kernel objects
|
||||
class kobject
|
||||
{
|
||||
@@ -39,6 +41,9 @@ public:
|
||||
/// \returns The object type for the koid
|
||||
static type koid_type(j6_koid_t koid);
|
||||
|
||||
/// Get this object's koid
|
||||
inline j6_koid_t koid() const { return m_koid; }
|
||||
|
||||
/// Set the given signals active on this object
|
||||
/// \arg s The set of signals to assert
|
||||
void assert_signal(j6_signal_t s);
|
||||
@@ -47,31 +52,6 @@ public:
|
||||
/// \arg s The set of signals to deassert
|
||||
void deassert_signal(j6_signal_t s);
|
||||
|
||||
class observer
|
||||
{
|
||||
public:
|
||||
/// Callback for when signals change.
|
||||
/// \arg obj The object triggering the callback
|
||||
/// \arg s The current state of obj's signals
|
||||
/// \arg ds Which signals caused the callback
|
||||
/// \arg result Status code for this notification
|
||||
/// \returns True if this object wants to keep watching signals
|
||||
virtual bool on_signals_changed(
|
||||
kobject *obj,
|
||||
j6_signal_t s,
|
||||
j6_signal_t ds,
|
||||
j6_status_t result) = 0;
|
||||
};
|
||||
|
||||
/// Register a signal observer
|
||||
/// \arg object The observer
|
||||
/// \arg s The signals the object wants notifiations for
|
||||
void register_signal_observer(observer *object, j6_signal_t s);
|
||||
|
||||
/// Deegister a signal observer
|
||||
/// \arg object The observer
|
||||
void deregister_signal_observer(observer *object);
|
||||
|
||||
/// Increment the handle refcount
|
||||
inline void handle_retain() { ++m_handle_count; }
|
||||
|
||||
@@ -80,31 +60,30 @@ public:
|
||||
if (--m_handle_count == 0) on_no_handles();
|
||||
}
|
||||
|
||||
/// Add the given thread to the list of threads waiting on this object.
|
||||
inline void add_blocked_thread(thread *t) { m_blocked_threads.append(t); }
|
||||
|
||||
/// Remove the given thread from the list of threads waiting on this object.
|
||||
inline void remove_blocked_thread(thread *t) { m_blocked_threads.remove_swap(t); }
|
||||
|
||||
protected:
|
||||
/// Interface for subclasses to handle when all handles are closed. Subclasses
|
||||
/// should either call the base version, or assert j6_signal_no_handles.
|
||||
virtual void on_no_handles();
|
||||
|
||||
private:
|
||||
kobject() = delete;
|
||||
kobject(const kobject &other) = delete;
|
||||
kobject(const kobject &&other) = delete;
|
||||
|
||||
/// Notifiy observers of this object
|
||||
/// \arg mask The signals that triggered this call. If 0, notify all observers.
|
||||
/// \arg result The result to pass to the observers
|
||||
void notify_signal_observers(j6_signal_t mask, j6_status_t result = j6_status_ok);
|
||||
|
||||
/// Interface for subclasses to handle when all handles are closed. Subclasses
|
||||
/// should either call the base version, or assert j6_signal_no_handles.
|
||||
virtual void on_no_handles();
|
||||
void notify_signal_observers();
|
||||
|
||||
j6_koid_t m_koid;
|
||||
j6_signal_t m_signals;
|
||||
uint16_t m_handle_count;
|
||||
|
||||
struct observer_registration
|
||||
{
|
||||
observer_registration(observer* o = nullptr, j6_signal_t s = 0) :
|
||||
object(o), signals(s) {}
|
||||
|
||||
observer *object;
|
||||
j6_signal_t signals;
|
||||
};
|
||||
kutil::vector<observer_registration> m_observers;
|
||||
protected:
|
||||
kutil::vector<thread*> m_blocked_threads;
|
||||
};
|
||||
|
||||
83
src/kernel/objects/thread.cpp
Normal file
83
src/kernel/objects/thread.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "j6/signals.h"
|
||||
#include "objects/thread.h"
|
||||
#include "scheduler.h"
|
||||
|
||||
static constexpr j6_signal_t thread_default_signals = 0;
|
||||
|
||||
thread::thread(page_table *pml4, priority_t pri) :
|
||||
kobject(kobject::type::thread, thread_default_signals),
|
||||
m_state(state::loading),
|
||||
m_wait_type(wait_type::none),
|
||||
m_wait_data(0),
|
||||
m_wait_obj(0)
|
||||
{
|
||||
TCB *tcbp = tcb();
|
||||
tcbp->pml4 = pml4;
|
||||
tcbp->priority = pri;
|
||||
tcbp->thread_data = this;
|
||||
set_state(state::ready);
|
||||
}
|
||||
|
||||
void
|
||||
thread::wait_on_signals(kobject *obj, j6_signal_t signals)
|
||||
{
|
||||
m_wait_type = wait_type::signal;
|
||||
m_wait_data = signals;
|
||||
clear_state(state::ready);
|
||||
}
|
||||
|
||||
void
|
||||
thread::wait_on_time(uint64_t t)
|
||||
{
|
||||
m_wait_type = wait_type::time;
|
||||
m_wait_data = t;
|
||||
clear_state(state::ready);
|
||||
}
|
||||
|
||||
bool
|
||||
thread::wake_on_signals(kobject *obj, j6_signal_t signals)
|
||||
{
|
||||
if (m_wait_type != wait_type::signal ||
|
||||
(signals & m_wait_data) == 0)
|
||||
return false;
|
||||
|
||||
m_wait_type = wait_type::none;
|
||||
m_wait_data = j6_status_ok;
|
||||
m_wait_obj = obj->koid();
|
||||
set_state(state::ready);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
thread::wake_on_time(uint64_t now)
|
||||
{
|
||||
if (m_wait_type != wait_type::time ||
|
||||
now < m_wait_data)
|
||||
return false;
|
||||
|
||||
m_wait_type = wait_type::none;
|
||||
m_wait_data = j6_status_ok;
|
||||
m_wait_obj = 0;
|
||||
set_state(state::ready);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
thread::wake_on_result(kobject *obj, j6_status_t result)
|
||||
{
|
||||
m_wait_type = wait_type::none;
|
||||
m_wait_data = result;
|
||||
m_wait_obj = obj->koid();
|
||||
set_state(state::ready);
|
||||
}
|
||||
|
||||
void
|
||||
thread::exit(uint32_t code)
|
||||
{
|
||||
// TODO: check if the process containing this thread
|
||||
// needs to exit and clean up.
|
||||
m_return_code = code;
|
||||
set_state(state::exited);
|
||||
clear_state(state::ready);
|
||||
}
|
||||
|
||||
133
src/kernel/objects/thread.h
Normal file
133
src/kernel/objects/thread.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
/// \file thread.h
|
||||
/// Definition of thread kobject types
|
||||
|
||||
#include "kutil/linked_list.h"
|
||||
#include "objects/kobject.h"
|
||||
|
||||
using priority_t = uint8_t;
|
||||
|
||||
struct page_table;
|
||||
|
||||
struct TCB
|
||||
{
|
||||
// Data used by assembly task control routines. If you change any of these,
|
||||
// be sure to change the assembly definitions in 'tasking.inc'
|
||||
uintptr_t rsp;
|
||||
uintptr_t rsp0;
|
||||
uintptr_t rsp3;
|
||||
page_table *pml4;
|
||||
|
||||
priority_t priority;
|
||||
// note: 3 bytes padding
|
||||
|
||||
// TODO: move state into TCB?
|
||||
|
||||
uintptr_t kernel_stack;
|
||||
size_t kernel_stack_size;
|
||||
|
||||
uint32_t time_left;
|
||||
uint64_t last_ran;
|
||||
|
||||
thread *thread_data;
|
||||
};
|
||||
|
||||
using tcb_list = kutil::linked_list<TCB>;
|
||||
using tcb_node = tcb_list::item_type;
|
||||
|
||||
class thread :
|
||||
public kobject
|
||||
{
|
||||
public:
|
||||
enum class wait_type : uint8_t { none, signal, time };
|
||||
enum class state : uint8_t {
|
||||
ready = 0x01,
|
||||
loading = 0x02,
|
||||
exited = 0x04,
|
||||
constant = 0x80,
|
||||
none = 0x00
|
||||
};
|
||||
|
||||
/// Constructor.
|
||||
/// \arg pml4 Root page table for the thread's owning process
|
||||
/// \arg pri Initial priority level of this thread
|
||||
thread(page_table *pml4, priority_t pri);
|
||||
|
||||
/// Get the `ready` state of the thread.
|
||||
/// \returns True if the thread is ready to execute.
|
||||
inline bool ready() const { return has_state(state::ready); }
|
||||
|
||||
/// Get the `loading` state of the thread.
|
||||
/// \returns True if the thread has not finished loading.
|
||||
inline bool loading() const { return has_state(state::loading); }
|
||||
|
||||
/// Get the `constant` state of the thread.
|
||||
/// \returns True if the thread has constant priority.
|
||||
inline bool constant() const { return has_state(state::constant); }
|
||||
|
||||
/// Get the thread priority.
|
||||
inline priority_t priority() const { return m_tcb.priority; }
|
||||
|
||||
/// Set the thread priority.
|
||||
/// \arg p The new thread priority
|
||||
inline void set_priority(priority_t p) { if (!constant()) m_tcb.priority = p; }
|
||||
|
||||
/// Block the thread, waiting on the given object's signals.
|
||||
/// \arg obj Object to wait on
|
||||
/// \arg signals Mask of signals to wait for
|
||||
void wait_on_signals(kobject *obj, j6_signal_t signals);
|
||||
|
||||
/// Block the thread, waiting for a given clock value
|
||||
/// \arg t Clock value to wait for
|
||||
void wait_on_time(uint64_t t);
|
||||
|
||||
/// Wake the thread if it is waiting on signals.
|
||||
/// \arg obj Object that changed signals
|
||||
/// \arg signals Signal state of the object
|
||||
/// \returns True if this action unblocked the thread
|
||||
bool wake_on_signals(kobject *obj, j6_signal_t signals);
|
||||
|
||||
/// Wake the thread if it is waiting on the clock.
|
||||
/// \arg now Current clock value
|
||||
/// \returns True if this action unblocked the thread
|
||||
bool wake_on_time(uint64_t now);
|
||||
|
||||
/// Wake the thread with a given result code.
|
||||
/// \arg obj Object that changed signals
|
||||
/// \arg result Result code to return to the thread
|
||||
void wake_on_result(kobject *obj, j6_status_t result);
|
||||
|
||||
inline bool has_state(state s) const {
|
||||
return static_cast<uint8_t>(m_state) & static_cast<uint8_t>(s);
|
||||
}
|
||||
|
||||
inline void set_state(state s) {
|
||||
m_state = static_cast<state>(static_cast<uint8_t>(m_state) | static_cast<uint8_t>(s));
|
||||
}
|
||||
|
||||
inline void clear_state(state s) {
|
||||
m_state = static_cast<state>(static_cast<uint8_t>(m_state) & ~static_cast<uint8_t>(s));
|
||||
}
|
||||
|
||||
inline tcb_node * tcb() { return &m_tcb; }
|
||||
|
||||
/// Terminate this thread.
|
||||
/// \arg code The return code to exit with.
|
||||
void exit(unsigned code);
|
||||
|
||||
private:
|
||||
thread() = delete;
|
||||
thread(const thread &other) = delete;
|
||||
thread(const thread &&other) = delete;
|
||||
|
||||
tcb_node m_tcb;
|
||||
|
||||
state m_state;
|
||||
wait_type m_wait_type;
|
||||
// There should be 1 byte of padding here
|
||||
|
||||
uint32_t m_return_code;
|
||||
|
||||
uint64_t m_wait_data;
|
||||
j6_koid_t m_wait_obj;
|
||||
};
|
||||
@@ -24,45 +24,97 @@ const uint64_t rflags_int = 0x202;
|
||||
|
||||
extern "C" {
|
||||
void ramdisk_process_loader();
|
||||
uintptr_t load_process_image(const void *image_start, size_t bytes, process *proc);
|
||||
uintptr_t load_process_image(const void *image_start, size_t bytes, TCB *tcb);
|
||||
};
|
||||
|
||||
extern uint64_t idle_stack_end;
|
||||
|
||||
/// Set up a new empty kernel stack for this thread. Sets rsp0 on the
|
||||
/// TCB object, but also returns it.
|
||||
/// \returns The new rsp0 as a pointer
|
||||
static void *
|
||||
setup_kernel_stack(TCB *tcb)
|
||||
{
|
||||
constexpr size_t initial_stack_size = 0x1000;
|
||||
constexpr unsigned null_frame_entries = 2;
|
||||
constexpr size_t null_frame_size = null_frame_entries * sizeof(uint64_t);
|
||||
|
||||
void *stack_bottom = kutil::kalloc(initial_stack_size);
|
||||
kutil::memset(stack_bottom, 0, initial_stack_size);
|
||||
|
||||
log::debug(logs::memory, "Created kernel stack at %016lx size 0x%lx",
|
||||
stack_bottom, initial_stack_size);
|
||||
|
||||
void *stack_top =
|
||||
kutil::offset_pointer(stack_bottom,
|
||||
initial_stack_size - null_frame_size);
|
||||
|
||||
uint64_t *null_frame = reinterpret_cast<uint64_t*>(stack_top);
|
||||
for (unsigned i = 0; i < null_frame_entries; ++i)
|
||||
null_frame[i] = 0;
|
||||
|
||||
tcb->kernel_stack_size = initial_stack_size;
|
||||
tcb->kernel_stack = reinterpret_cast<uintptr_t>(stack_bottom);
|
||||
tcb->rsp0 = reinterpret_cast<uintptr_t>(stack_top);
|
||||
tcb->rsp = tcb->rsp0;
|
||||
|
||||
return stack_top;
|
||||
}
|
||||
|
||||
/// Initialize this process' kenrel stack with a fake return segment for
|
||||
/// returning out of task_switch.
|
||||
/// \arg tcb TCB of the thread to modify
|
||||
/// \arg rip The rip to return to
|
||||
static void
|
||||
add_fake_task_return(TCB *tcb, uintptr_t rip)
|
||||
{
|
||||
tcb->rsp -= sizeof(uintptr_t) * 7;
|
||||
uintptr_t *stack = reinterpret_cast<uintptr_t*>(tcb->rsp);
|
||||
|
||||
stack[6] = rip; // return rip
|
||||
stack[5] = tcb->rsp0; // rbp
|
||||
stack[4] = 0xbbbbbbbb; // rbx
|
||||
stack[3] = 0x12121212; // r12
|
||||
stack[2] = 0x13131313; // r13
|
||||
stack[1] = 0x14141414; // r14
|
||||
stack[0] = 0x15151515; // r15
|
||||
}
|
||||
|
||||
scheduler::scheduler(lapic *apic) :
|
||||
m_apic(apic),
|
||||
m_next_pid(1),
|
||||
m_clock(0)
|
||||
{
|
||||
auto *idle = new process_node;
|
||||
page_table *pml4 = page_manager::get_pml4();
|
||||
thread *idle = new thread(pml4, max_priority);
|
||||
|
||||
auto *tcb = idle->tcb();
|
||||
|
||||
log::debug(logs::task, "Idle thread koid %llx", idle->koid());
|
||||
|
||||
// The kernel idle task, also the thread we're in now
|
||||
idle->pid = 0;
|
||||
idle->ppid = 0;
|
||||
idle->priority = max_priority;
|
||||
idle->rsp = 0; // This will get set when we switch away
|
||||
idle->rsp3 = 0; // Never used for the idle task
|
||||
idle->rsp0 = reinterpret_cast<uintptr_t>(&idle_stack_end);
|
||||
idle->pml4 = page_manager::get_pml4();
|
||||
idle->flags =
|
||||
process_flags::running |
|
||||
process_flags::ready |
|
||||
process_flags::const_pri;
|
||||
tcb->rsp = 0; // This will get set when we switch away
|
||||
tcb->rsp3 = 0; // Never used for the idle task
|
||||
tcb->rsp0 = reinterpret_cast<uintptr_t>(&idle_stack_end);
|
||||
|
||||
m_runlists[max_priority].push_back(idle);
|
||||
m_current = idle;
|
||||
idle->set_state(thread::state::constant);
|
||||
|
||||
bsp_cpu_data.rsp0 = idle->rsp0;
|
||||
bsp_cpu_data.tcb = idle;
|
||||
m_runlists[max_priority].push_back(tcb);
|
||||
m_current = tcb;
|
||||
|
||||
bsp_cpu_data.rsp0 = tcb->rsp0;
|
||||
bsp_cpu_data.tcb = tcb;
|
||||
}
|
||||
|
||||
uintptr_t
|
||||
load_process_image(const void *image_start, size_t bytes, process *proc)
|
||||
load_process_image(const void *image_start, size_t bytes, TCB *tcb)
|
||||
{
|
||||
// We're now in the process space for this process, allocate memory for the
|
||||
// process code and load it
|
||||
page_manager *pager = page_manager::get();
|
||||
|
||||
thread *th = tcb->thread_data;
|
||||
|
||||
log::debug(logs::loader, "Loading task! ELF: %016lx [%d]", image_start, bytes);
|
||||
|
||||
// TODO: Handle bad images gracefully
|
||||
@@ -108,33 +160,30 @@ load_process_image(const void *image_start, size_t bytes, process *proc)
|
||||
kutil::memcpy(dest, src, header->size);
|
||||
}
|
||||
|
||||
proc->flags &= ~process_flags::loading;
|
||||
th->clear_state(thread::state::loading);
|
||||
|
||||
uintptr_t entrypoint = image.entrypoint();
|
||||
log::debug(logs::loader, " Loaded! New process rip: %016lx", entrypoint);
|
||||
log::debug(logs::loader, " Loaded! New thread rip: %016lx", entrypoint);
|
||||
return entrypoint;
|
||||
}
|
||||
|
||||
process_node *
|
||||
scheduler::create_process(pid_t pid)
|
||||
thread *
|
||||
scheduler::create_process(page_table *pml4)
|
||||
{
|
||||
kassert(pid <= 0, "Cannot specify a positive pid in create_process");
|
||||
thread *th = new thread(pml4, default_priority);
|
||||
auto *tcb = th->tcb();
|
||||
|
||||
auto *proc = new process_node;
|
||||
proc->pid = pid ? pid : m_next_pid++;
|
||||
proc->priority = default_priority;
|
||||
proc->time_left = quantum(default_priority) + startup_bonus;
|
||||
tcb->time_left = quantum(default_priority) + startup_bonus;
|
||||
|
||||
log::debug(logs::task, "Creating process %d, priority %d, time slice %d",
|
||||
proc->pid, proc->priority, proc->time_left);
|
||||
log::debug(logs::task, "Creating thread %llx, priority %d, time slice %d",
|
||||
th->koid(), tcb->priority, tcb->time_left);
|
||||
|
||||
return proc;
|
||||
return th;
|
||||
}
|
||||
|
||||
void
|
||||
scheduler::load_process(const char *name, const void *data, size_t size)
|
||||
{
|
||||
auto *proc = create_process();
|
||||
|
||||
uint16_t kcs = (1 << 3) | 0; // Kernel CS is GDT entry 1, ring 0
|
||||
uint16_t cs = (5 << 3) | 3; // User CS is GDT entry 5, ring 3
|
||||
@@ -143,19 +192,22 @@ scheduler::load_process(const char *name, const void *data, size_t size)
|
||||
uint16_t ss = (4 << 3) | 3; // User SS is GDT entry 4, ring 3
|
||||
|
||||
// Set up the page tables - this also allocates an initial user stack
|
||||
proc->pml4 = page_manager::get()->create_process_map();
|
||||
page_table *pml4 = page_manager::get()->create_process_map();
|
||||
|
||||
thread* th = create_process(pml4);
|
||||
auto *tcb = th->tcb();
|
||||
|
||||
// Create an initial kernel stack space
|
||||
void *sp0 = proc->setup_kernel_stack();
|
||||
void *sp0 = setup_kernel_stack(tcb);
|
||||
uintptr_t *stack = reinterpret_cast<uintptr_t *>(sp0) - 7;
|
||||
|
||||
// Pass args to ramdisk_process_loader on the stack
|
||||
stack[0] = reinterpret_cast<uintptr_t>(data);
|
||||
stack[1] = reinterpret_cast<uintptr_t>(size);
|
||||
stack[2] = reinterpret_cast<uintptr_t>(proc);
|
||||
stack[2] = reinterpret_cast<uintptr_t>(tcb);
|
||||
|
||||
proc->rsp = reinterpret_cast<uintptr_t>(stack);
|
||||
proc->add_fake_task_return(
|
||||
tcb->rsp = reinterpret_cast<uintptr_t>(stack);
|
||||
add_fake_task_return(tcb,
|
||||
reinterpret_cast<uintptr_t>(ramdisk_process_loader));
|
||||
|
||||
// Arguments for iret - rip will be pushed on before these
|
||||
@@ -164,46 +216,42 @@ scheduler::load_process(const char *name, const void *data, size_t size)
|
||||
stack[5] = initial_stack;
|
||||
stack[6] = ss;
|
||||
|
||||
proc->rsp3 = initial_stack;
|
||||
proc->flags =
|
||||
process_flags::running |
|
||||
process_flags::ready |
|
||||
process_flags::loading;
|
||||
tcb->rsp3 = initial_stack;
|
||||
|
||||
m_runlists[default_priority].push_back(proc);
|
||||
m_runlists[default_priority].push_back(tcb);
|
||||
|
||||
log::debug(logs::task, "Creating process %s: pid %d pri %d", name, proc->pid, proc->priority);
|
||||
log::debug(logs::task, " RSP %016lx", proc->rsp);
|
||||
log::debug(logs::task, " RSP0 %016lx", proc->rsp0);
|
||||
log::debug(logs::task, " PML4 %016lx", proc->pml4);
|
||||
log::debug(logs::task, "Loading thread %s: koid %llx pri %d", name, th->koid(), tcb->priority);
|
||||
log::debug(logs::task, " RSP %016lx", tcb->rsp);
|
||||
log::debug(logs::task, " RSP0 %016lx", tcb->rsp0);
|
||||
log::debug(logs::task, " PML4 %016lx", tcb->pml4);
|
||||
}
|
||||
|
||||
void
|
||||
scheduler::create_kernel_task(pid_t pid, void (*task)(), uint8_t priority, process_flags flags)
|
||||
scheduler::create_kernel_task(void (*task)(), uint8_t priority, bool constant)
|
||||
{
|
||||
auto *proc = create_process(pid);
|
||||
page_table *pml4 = page_manager::get()->get_kernel_pml4();
|
||||
thread *th = create_process(pml4);
|
||||
auto *tcb = th->tcb();
|
||||
|
||||
uint16_t kcs = (1 << 3) | 0; // Kernel CS is GDT entry 1, ring 0
|
||||
uint16_t kss = (2 << 3) | 0; // Kernel SS is GDT entry 2, ring 0
|
||||
|
||||
// Create an initial kernel stack space
|
||||
proc->setup_kernel_stack();
|
||||
proc->add_fake_task_return(
|
||||
setup_kernel_stack(tcb);
|
||||
add_fake_task_return(tcb,
|
||||
reinterpret_cast<uintptr_t>(task));
|
||||
|
||||
proc->priority = priority;
|
||||
proc->pml4 = page_manager::get()->get_kernel_pml4();
|
||||
proc->flags =
|
||||
process_flags::running |
|
||||
process_flags::ready |
|
||||
flags;
|
||||
tcb->priority = priority;
|
||||
tcb->pml4 = page_manager::get()->get_kernel_pml4();
|
||||
if (constant)
|
||||
th->set_state(thread::state::constant);
|
||||
|
||||
m_runlists[default_priority].push_back(proc);
|
||||
m_runlists[default_priority].push_back(tcb);
|
||||
|
||||
log::debug(logs::task, "Creating kernel task: pid %d pri %d", proc->pid, proc->priority);
|
||||
log::debug(logs::task, " RSP0 %016lx", proc->rsp0);
|
||||
log::debug(logs::task, " RSP %016lx", proc->rsp);
|
||||
log::debug(logs::task, " PML4 %016lx", proc->pml4);
|
||||
log::debug(logs::task, "Creating kernel task: thread %llx pri %d", th->koid(), tcb->priority);
|
||||
log::debug(logs::task, " RSP0 %016lx", tcb->rsp0);
|
||||
log::debug(logs::task, " RSP %016lx", tcb->rsp);
|
||||
log::debug(logs::task, " PML4 %016lx", tcb->pml4);
|
||||
}
|
||||
|
||||
uint32_t
|
||||
@@ -223,45 +271,48 @@ scheduler::start()
|
||||
|
||||
void scheduler::prune(uint64_t now)
|
||||
{
|
||||
// TODO: Promote processes that haven't been scheduled in too long
|
||||
|
||||
// Find processes that aren't ready or aren't running and
|
||||
// move them to the appropriate lists.
|
||||
for (auto &pri_list : m_runlists) {
|
||||
auto *proc = pri_list.front();
|
||||
while (proc) {
|
||||
uint64_t age = now - proc->last_ran;
|
||||
process_flags flags = proc->flags;
|
||||
uint8_t priority = proc->priority;
|
||||
auto *tcb = pri_list.front();
|
||||
while (tcb) {
|
||||
thread *th = tcb->thread_data;
|
||||
uint64_t age = now - tcb->last_ran;
|
||||
uint8_t priority = tcb->priority;
|
||||
|
||||
bool running = flags && process_flags::running;
|
||||
bool ready = flags && process_flags::ready;
|
||||
bool ready = th->has_state(thread::state::ready);
|
||||
bool constant = th->has_state(thread::state::constant);
|
||||
|
||||
bool stale = age > quantum(priority) * 2 &&
|
||||
proc->priority > promote_limit &&
|
||||
!(flags && process_flags::const_pri);
|
||||
tcb->priority > promote_limit &&
|
||||
!constant;
|
||||
|
||||
if (running && ready) {
|
||||
auto *remove = proc;
|
||||
proc = proc->next();
|
||||
if (ready) {
|
||||
auto *remove = tcb;
|
||||
tcb = tcb->next();
|
||||
|
||||
if (stale) {
|
||||
m_runlists[remove->priority].remove(remove);
|
||||
remove->priority -= 1;
|
||||
remove->time_left = quantum(remove->priority);
|
||||
m_runlists[remove->priority].push_back(remove);
|
||||
log::debug(logs::task, "Scheduler promoting process %d, priority %d",
|
||||
remove->pid, remove->priority);
|
||||
log::debug(logs::task, "Scheduler promoting thread %llx, priority %d",
|
||||
th->koid(), remove->priority);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *remove = proc;
|
||||
proc = proc->next();
|
||||
auto *remove = tcb;
|
||||
tcb = tcb->next();
|
||||
pri_list.remove(remove);
|
||||
|
||||
if (!(remove->flags && process_flags::running)) {
|
||||
bool exited = th->has_state(thread::state::exited);
|
||||
|
||||
if (exited) {
|
||||
// TODO: Alert continaing process thread exitied,
|
||||
// and exit process if it was the last thread.
|
||||
/*
|
||||
auto *parent = get_process_by_id(remove->ppid);
|
||||
if (parent && parent->wake_on_child(remove)) {
|
||||
m_blocked.remove(parent);
|
||||
@@ -270,7 +321,10 @@ void scheduler::prune(uint64_t now)
|
||||
} else {
|
||||
m_exited.push_back(remove);
|
||||
}
|
||||
*/
|
||||
m_exited.push_back(remove);
|
||||
} else {
|
||||
log::debug(logs::task, "Prune: moving blocked thread %llx", th->koid());
|
||||
m_blocked.push_back(remove);
|
||||
}
|
||||
}
|
||||
@@ -278,13 +332,15 @@ void scheduler::prune(uint64_t now)
|
||||
|
||||
// Find blocked processes that are ready (possibly after waking wating
|
||||
// ones) and move them to the appropriate runlist.
|
||||
auto *proc = m_blocked.front();
|
||||
while (proc) {
|
||||
bool ready = proc->flags && process_flags::ready;
|
||||
ready |= proc->wake_on_time(now);
|
||||
auto *tcb = m_blocked.front();
|
||||
while (tcb) {
|
||||
thread *th = tcb->thread_data;
|
||||
|
||||
auto *remove = proc;
|
||||
proc = proc->next();
|
||||
bool ready = th->has_state(thread::state::ready);
|
||||
ready |= th->wake_on_time(now);
|
||||
|
||||
auto *remove = tcb;
|
||||
tcb = tcb->next();
|
||||
if (!ready) continue;
|
||||
|
||||
m_blocked.remove(remove);
|
||||
@@ -295,17 +351,19 @@ void scheduler::prune(uint64_t now)
|
||||
void
|
||||
scheduler::schedule()
|
||||
{
|
||||
pid_t lastpid = m_current->pid;
|
||||
uint8_t priority = m_current->priority;
|
||||
uint32_t remaining = m_apic->stop_timer();
|
||||
m_current->time_left = remaining;
|
||||
thread *th = m_current->thread_data;
|
||||
const bool constant = th->has_state(thread::state::constant);
|
||||
|
||||
if (remaining == 0 && priority < max_priority &&
|
||||
!(m_current->flags && process_flags::const_pri)) {
|
||||
// Process used its whole timeslice, demote it
|
||||
++m_current->priority;
|
||||
log::debug(logs::task, "Scheduler demoting process %d, priority %d",
|
||||
m_current->pid, m_current->priority);
|
||||
if (remaining == 0) {
|
||||
if (priority < max_priority && !constant) {
|
||||
// Process used its whole timeslice, demote it
|
||||
++m_current->priority;
|
||||
log::debug(logs::task, "Scheduler demoting thread %llx, priority %d",
|
||||
th->koid(), m_current->priority);
|
||||
}
|
||||
m_current->time_left = quantum(m_current->priority);
|
||||
} else if (remaining > 0) {
|
||||
// Process gave up CPU, give it a small bonus to its
|
||||
@@ -315,7 +373,7 @@ scheduler::schedule()
|
||||
}
|
||||
|
||||
m_runlists[priority].remove(m_current);
|
||||
if (m_current->flags && process_flags::ready) {
|
||||
if (th->has_state(thread::state::ready)) {
|
||||
m_runlists[m_current->priority].push_back(m_current);
|
||||
} else {
|
||||
m_blocked.push_back(m_current);
|
||||
@@ -332,18 +390,20 @@ scheduler::schedule()
|
||||
|
||||
m_current->last_ran = m_clock;
|
||||
|
||||
m_current = m_runlists[priority].pop_front();
|
||||
m_current->last_ran = m_clock;
|
||||
m_apic->reset_timer(m_current->time_left);
|
||||
auto *next = m_runlists[priority].pop_front();
|
||||
next->last_ran = m_clock;
|
||||
m_apic->reset_timer(next->time_left);
|
||||
|
||||
if (lastpid != m_current->pid) {
|
||||
if (next != m_current) {
|
||||
m_current = next;
|
||||
task_switch(m_current);
|
||||
|
||||
log::debug(logs::task, "Scheduler switched to process %d, priority %d time left %d @ %lld.",
|
||||
m_current->pid, m_current->priority, m_current->time_left, m_clock);
|
||||
log::debug(logs::task, "Scheduler switched to thread %llx, priority %d time left %d @ %lld.",
|
||||
th->koid(), m_current->priority, m_current->time_left, m_clock);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
process_node *
|
||||
scheduler::get_process_by_id(uint32_t pid)
|
||||
{
|
||||
@@ -364,3 +424,4 @@ scheduler::get_process_by_id(uint32_t pid)
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
/// The task scheduler and related definitions
|
||||
|
||||
#include <stdint.h>
|
||||
#include "process.h"
|
||||
#include "objects/thread.h"
|
||||
|
||||
class lapic;
|
||||
struct page_table;
|
||||
struct cpu_state;
|
||||
|
||||
extern "C" void isr_handler(cpu_state*);
|
||||
extern "C" void task_switch(process *next);
|
||||
extern "C" void task_fork(process *child);
|
||||
extern "C" void task_switch(TCB *next);
|
||||
|
||||
|
||||
/// The task scheduler
|
||||
@@ -27,7 +26,7 @@ public:
|
||||
/// Default priority on process creation
|
||||
static const uint8_t default_priority = 1;
|
||||
|
||||
/// Loest (most urgent) priority achieved via promotion
|
||||
/// Lowest (most urgent) priority achieved via promotion
|
||||
static const uint8_t promote_limit = 1;
|
||||
|
||||
/// How long the base timer quantum is, in us
|
||||
@@ -50,15 +49,13 @@ public:
|
||||
void load_process(const char *name, const void *data, size_t size);
|
||||
|
||||
/// Create a new kernel task
|
||||
/// \arg pid Pid to use for this task, must be negative
|
||||
/// \arg proc Function to run as a kernel task
|
||||
/// \arg priority Priority to start the process with
|
||||
/// \arg flags Flags to add to the process
|
||||
/// \arg constant True if this task cannot be promoted/demoted
|
||||
void create_kernel_task(
|
||||
pid_t pid,
|
||||
void (*task)(),
|
||||
uint8_t priority,
|
||||
process_flags flags = process_flags::none);
|
||||
bool constant = false);
|
||||
|
||||
/// Get the quantum for a given priority.
|
||||
uint32_t quantum(int priority);
|
||||
@@ -70,14 +67,16 @@ public:
|
||||
/// Run the scheduler, possibly switching to a new task
|
||||
void schedule();
|
||||
|
||||
/// Get the current process.
|
||||
/// \returns A pointer to the current process' process struct
|
||||
inline process * current() { return m_current; }
|
||||
/// Get the current TCB.
|
||||
/// \returns A pointer to the current thread's TCB
|
||||
inline TCB * current() { return m_current; }
|
||||
|
||||
/*
|
||||
/// Look up a process by its PID
|
||||
/// \arg pid The requested PID
|
||||
/// \returns The process matching that PID, or nullptr
|
||||
process_node * get_process_by_id(uint32_t pid);
|
||||
tcb_node * get_process_by_id(uint32_t pid);
|
||||
*/
|
||||
|
||||
/// Get a reference to the system scheduler
|
||||
/// \returns A reference to the global system scheduler
|
||||
@@ -89,9 +88,9 @@ private:
|
||||
|
||||
/// Create a new process object. This process will have its pid
|
||||
/// set but nothing else.
|
||||
/// \arg pid The pid to give the process (0 for automatic)
|
||||
/// \returns The new process object
|
||||
process_node * create_process(pid_t pid = 0);
|
||||
/// \arg pml4 The root page table of the process
|
||||
/// \returns The new process' main thread
|
||||
thread * create_process(page_table *pml4);
|
||||
|
||||
void prune(uint64_t now);
|
||||
|
||||
@@ -100,10 +99,10 @@ private:
|
||||
uint32_t m_next_pid;
|
||||
uint32_t m_tick_count;
|
||||
|
||||
process_node *m_current;
|
||||
process_list m_runlists[num_priorities];
|
||||
process_list m_blocked;
|
||||
process_list m_exited;
|
||||
tcb_node *m_current;
|
||||
tcb_list m_runlists[num_priorities];
|
||||
tcb_list m_blocked;
|
||||
tcb_list m_exited;
|
||||
|
||||
// TODO: lol a real clock
|
||||
uint64_t m_clock = 0;
|
||||
|
||||
@@ -2,8 +2,8 @@ SYSCALL(0x00, object_noop, void)
|
||||
SYSCALL(0x01, object_wait, j6_handle_t, j6_signal_t, j6_signal_t *)
|
||||
|
||||
SYSCALL(0x11, process_exit, int64_t)
|
||||
SYSCALL(0x12, process_fork, pid_t*)
|
||||
SYSCALL(0x13, process_getpid, pid_t*)
|
||||
SYSCALL(0x12, process_fork, uint32_t*)
|
||||
SYSCALL(0x13, process_getpid, uint32_t*)
|
||||
SYSCALL(0x14, process_log, const char *)
|
||||
SYSCALL(0x15, process_pause, void)
|
||||
SYSCALL(0x16, process_sleep, uint64_t)
|
||||
|
||||
@@ -10,8 +10,9 @@ j6_status_t
|
||||
object_noop()
|
||||
{
|
||||
auto &s = scheduler::get();
|
||||
auto *p = s.current();
|
||||
log::debug(logs::syscall, "Process %d called noop syscall.", p->pid);
|
||||
TCB *tcb = s.current();
|
||||
thread *th = tcb->thread_data;
|
||||
log::debug(logs::syscall, "Thread %llx called noop syscall.", th->koid());
|
||||
return j6_status_ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,18 @@ j6_status_t
|
||||
process_exit(int64_t status)
|
||||
{
|
||||
auto &s = scheduler::get();
|
||||
auto *p = s.current();
|
||||
log::debug(logs::syscall, "Process %d exiting with code %d", p->pid, status);
|
||||
TCB *tcb = s.current();
|
||||
thread *th = tcb->thread_data;
|
||||
log::debug(logs::syscall, "Thread %llx exiting with code %d", th->koid(), status);
|
||||
|
||||
p->exit(status);
|
||||
th->exit(status);
|
||||
s.schedule();
|
||||
|
||||
log::error(logs::syscall, "returned to exit syscall");
|
||||
return j6_err_unexpected;
|
||||
}
|
||||
|
||||
/*
|
||||
j6_status_t
|
||||
process_fork(pid_t *pid)
|
||||
{
|
||||
@@ -54,6 +56,7 @@ process_getpid(pid_t *pid)
|
||||
*pid = p->pid;
|
||||
return j6_status_ok;
|
||||
}
|
||||
*/
|
||||
|
||||
j6_status_t
|
||||
process_log(const char *message)
|
||||
@@ -63,17 +66,22 @@ process_log(const char *message)
|
||||
}
|
||||
|
||||
auto &s = scheduler::get();
|
||||
auto *p = s.current();
|
||||
log::info(logs::syscall, "Message[%d]: %s", p->pid, message);
|
||||
TCB *tcb = s.current();
|
||||
thread *th = tcb->thread_data;
|
||||
log::info(logs::syscall, "Message[%llx]: %s", th->koid(), message);
|
||||
return j6_status_ok;
|
||||
}
|
||||
|
||||
j6_status_t process_fork(uint32_t *pid) { *pid = 5; return process_log("CALLED FORK"); }
|
||||
j6_status_t process_getpid(uint32_t *pid) { *pid = 0; return process_log("CALLED GETPID"); }
|
||||
|
||||
j6_status_t
|
||||
process_pause()
|
||||
{
|
||||
auto &s = scheduler::get();
|
||||
auto *p = s.current();
|
||||
p->wait_on_signal(-1ull);
|
||||
TCB *tcb = s.current();
|
||||
thread *th = tcb->thread_data;
|
||||
th->wait_on_signals(th, -1ull);
|
||||
s.schedule();
|
||||
return j6_status_ok;
|
||||
}
|
||||
@@ -82,10 +90,11 @@ j6_status_t
|
||||
process_sleep(uint64_t til)
|
||||
{
|
||||
auto &s = scheduler::get();
|
||||
auto *p = s.current();
|
||||
log::debug(logs::syscall, "Process %d sleeping until %llu", p->pid, til);
|
||||
TCB *tcb = s.current();
|
||||
thread *th = tcb->thread_data;
|
||||
log::debug(logs::syscall, "Thread %llx sleeping until %llu", th->koid(), til);
|
||||
|
||||
p->wait_on_time(til);
|
||||
th->wait_on_time(til);
|
||||
s.schedule();
|
||||
return j6_status_ok;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,28 @@ public:
|
||||
m_elements[m_size].~T();
|
||||
}
|
||||
|
||||
/// Remove the first occurance of an item from the array, not
|
||||
/// order-preserving. Does nothing if the item is not in the array.
|
||||
void remove_swap(const T &item)
|
||||
{
|
||||
for (size_t i = 0; i < m_size; ++i) {
|
||||
if (m_elements[i] == item) {
|
||||
remove_swap_at(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the item at the given index from the array, not
|
||||
/// order-preserving.
|
||||
void remove_swap_at(size_t i)
|
||||
{
|
||||
if (i >= count()) return;
|
||||
if (i < m_size - 1)
|
||||
m_elements[i] = m_elements[m_size - 1];
|
||||
remove();
|
||||
}
|
||||
|
||||
/// Remove an item from the end of the array and return it.
|
||||
T pop()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user