【Asterinas】Asterinas 进程启动与切换
Asterinas 进程启动与切换
进程启动
进程创建:
Rust
pub fn spawn_user_process(
executable_path: &str,
argv: Vec,
envp: Vec,
) -> Result<Arc> {
// spawn user process should give an absolute path
debug_assert!(executable_path.starts_with(‘/’));
let process = Process::create_user_process(executable_path, argv, envp)?;
open_ntty_as_controlling_terminal(&process)?;process.run();Ok(process)
}
Rust
fn create_user_process(
executable_path: &str,
argv: Vec,
envp: Vec,
) -> Result<Arc> {
let process_builder = {
let pid = allocate_tid();
let parent = Weak::new();
let credentials = Credentials::new_root();let mut builder = ProcessBuilder::new(pid, executable_path, parent);builder.argv(argv).envp(envp).credentials(credentials);builder};let process = process_builder.build()?;// Lock order: session table -> group table -> process table -> group of process// -> group inner -> session innerlet mut session_table_mut = process_table::session_table_mut();let mut group_table_mut = process_table::group_table_mut();let mut process_table_mut = process_table::process_table_mut();// Creates new grouplet group = ProcessGroup::new(process.clone());*process.process_group.lock() = Arc::downgrade(&group);group_table_mut.insert(group.pgid(), group.clone());// Creates new sessionlet session = Session::new(group.clone());group.inner.lock().session = Arc::downgrade(&session);session.inner.lock().leader = Some(process.clone());session_table_mut.insert(session.sid(), session);process_table_mut.insert(process.pid(), process.clone());Ok(process)
}
创建线程:
Rust
pub fn build(self) -> Result<Arc> {
self.check_build()?;
let Self {
pid,
executable_path,
parent,
main_thread_builder,
argv,
envp,
process_vm,
file_table,
fs,
umask,
resource_limits,
sig_dispositions,
credentials,
} = self;
let process_vm = process_vm.or_else(|| Some(ProcessVm::alloc())).unwrap();let file_table = file_table.or_else(|| Some(Arc::new(Mutex::new(FileTable::new_with_stdio())))).unwrap();let fs = fs.or_else(|| Some(Arc::new(RwMutex::new(FsResolver::new())))).unwrap();let umask = umask.or_else(|| Some(Arc::new(RwLock::new(FileCreationMask::default())))).unwrap();let resource_limits = resource_limits.or_else(|| Some(ResourceLimits::default())).unwrap();let sig_dispositions = sig_dispositions.or_else(|| Some(Arc::new(Mutex::new(SigDispositions::new())))).unwrap();let process = {let threads = Vec::new();Arc::new(Process::new(pid,parent,threads,executable_path.to_string(),process_vm,file_table,fs,umask,sig_dispositions,resource_limits,))};let thread = if let Some(thread_builder) = main_thread_builder {let builder = thread_builder.process(Arc::downgrade(&process));builder.build()} else {Thread::new_posix_thread_from_executable(pid,credentials.unwrap(),process.vm(),&process.fs().read(),executable_path,Arc::downgrade(&process),argv.unwrap(),envp.unwrap(),)?};process.threads().lock().push(thread);process.set_runnable();Ok(process)
}
Rust
impl PosixThreadExt for Thread {
/// This function should only be called when launch shell()
fn new_posix_thread_from_executable(
tid: Tid,
credentials: Credentials,
process_vm: &ProcessVm,
fs_resolver: &FsResolver,
executable_path: &str,
process: Weak,
argv: Vec,
envp: Vec,
) -> Result<Arc> {
let elf_file = {
let fs_path = FsPath::new(AT_FDCWD, executable_path)?;
fs_resolver.lookup(&fs_path)?
};
let (_, elf_load_info) =
load_program_to_vm(process_vm, elf_file, argv, envp, fs_resolver, 1)?;
let vm_space = process_vm.root_vmar().vm_space().clone();let mut cpu_ctx = UserContext::default();cpu_ctx.set_rip(elf_load_info.entry_point() as _);cpu_ctx.set_rsp(elf_load_info.user_stack_top() as _);let user_space = Arc::new(UserSpace::new(vm_space, cpu_ctx));let thread_name = Some(ThreadName::new_from_executable_path(executable_path)?);let thread_builder = PosixThreadBuilder::new(tid, user_space, credentials).thread_name(thread_name).process(process);Ok(thread_builder.build())
}
从ELF文件中解析装载到内存中,并且找到ELF文件的入口地址,并且设置相应的栈:
Rust
cpu_ctx.set_rip(elf_load_info.entry_point() as _);
cpu_ctx.set_rsp(elf_load_info.user_stack_top() as _);
vm_space与cpu_ctx都被塞进user_space 中。
Rust
let user_space = Arc::new(UserSpace::new(vm_space, cpu_ctx));
创建新线程:
Rust
pub fn build(self) -> Arc {
let Self {
tid,
user_space,
process,
credentials,
thread_name,
set_child_tid,
clear_child_tid,
sig_mask,
sig_queues,
is_main_thread,
} = self;
let thread = Arc::new_cyclic(|thread_ref| {
let task = create_new_user_task(user_space, thread_ref.clone());
let status = ThreadStatus::Init;
let posix_thread = PosixThread {
process,
is_main_thread,
name: Mutex::new(thread_name),
set_child_tid: Mutex::new(set_child_tid),
clear_child_tid: Mutex::new(clear_child_tid),
credentials,
sig_mask: Mutex::new(sig_mask),
sig_queues: Mutex::new(sig_queues),
sig_context: Mutex::new(None),
sig_stack: Mutex::new(None),
robust_list: Mutex::new(None),
};
Thread::new(tid, task, posix_thread, status)});thread_table::add_thread(thread.clone());thread
}
这里面核心是创建task,task被丢到thread中:
Rust
let task = create_new_user_task(user_space, thread_ref.clone());
let status = ThreadStatus::Init;
let posix_thread = PosixThread {
process,
is_main_thread,
name: Mutex::new(thread_name),
set_child_tid: Mutex::new(set_child_tid),
clear_child_tid: Mutex::new(clear_child_tid),
credentials,
sig_mask: Mutex::new(sig_mask),
sig_queues: Mutex::new(sig_queues),
sig_context: Mutex::new(None),
sig_stack: Mutex::new(None),
robust_list: Mutex::new(None),
};
Thread::new(tid, task, posix_thread, status)
create_new_user_task实现:
Rust
pub fn create_new_user_task(user_space: Arc, thread_ref: Weak) -> Arc {
fn user_task_entry() {
let cur = Task::current();
let user_space = cur.user_space().expect(“user task should have user space”);
let mut user_mode = UserMode::new(user_space);
debug!(
“[Task entry] rip = 0x{:x}”,
user_mode.context().instruction_pointer()
);
debug!(
“[Task entry] rsp = 0x{:x}”,
user_mode.context().stack_pointer()
);
debug!(
“[Task entry] rax = 0x{:x}”,
user_mode.context().syscall_ret()
);
loop {
let user_event: UserEvent = user_mode.execute();
let context = user_mode.context_mut();
// handle user event:
handle_user_event(user_event, context);
let current_thread = current_thread!();
// should be do this comparison before handle signal?
if current_thread.status().lock().is_exited() {
break;
}
handle_pending_signal(context).unwrap();
if current_thread.status().lock().is_exited() {
debug!(“exit due to signal”);
break;
}
// If current is suspended, wait for a signal to wake up self
while current_thread.status().lock().is_stopped() {
Thread::yield_now();
debug!(“{} is suspended.”, current_thread.tid());
handle_pending_signal(context).unwrap();
}
// a preemption point after handling user event.
preempt();
}
debug!(“exit user loop”);
// FIXME: This is a work around: exit in kernel task entry may be not called. Why this will happen?
Task::current().exit();
}
TaskOptions::new(user_task_entry).data(thread_ref).user_space(Some(user_space)).build().expect("spawn task failed")
}
从user_space钟创建user_mode
Rust
let mut user_mode = UserMode::new(user_space);
其实就是将user_space中的context转移给user_mode:
Rust
pub fn new(user_space: &'a Arc) -> Self {
Self {
current: Task::current(),
user_space,
context: user_space.init_ctx,
}
}
user_task_entry作为当前task的用户入口。同时构建task的kernel侧入口kernel_task_entry,kernel_task_entry被设置到task的TaskContext中:
Rust
pub fn build(self) -> Result<Arc> {
/// all task will entering this function
/// this function is mean to executing the task_fn in Task
fn kernel_task_entry() {
let current_task = current_task()
.expect(“no current task, it should have current task in kernel task entry”);
current_task.func.call(());
current_task.exit();
}
let result = Task {
func: self.func.unwrap(),
data: self.data.unwrap(),
user_space: self.user_space,
task_inner: Mutex::new(TaskInner {
task_status: TaskStatus::Runnable,
ctx: TaskContext::default(),
}),
exit_code: 0,
kstack: KernelStack::new_with_guard_page()?,
link: LinkedListAtomicLink::new(),
priority: self.priority,
cpu_affinity: self.cpu_affinity,
};
result.task_inner.lock().task_status = TaskStatus::Runnable;result.task_inner.lock().ctx.rip = kernel_task_entry as usize;result.task_inner.lock().ctx.regs.rsp =(crate::vm::paddr_to_vaddr(result.kstack.end_paddr())) as u64;Ok(Arc::new(result))
}
从上面的逻辑很清晰的流程,从创建进程process到创建线程thread到创建任务task.
Task任务切换
从上面的流程中,已经创建好了task。现在看看task如何切换以及执行。
Rust
process.run();
进程创建好后,开始执行,线程开始执行
Rust
pub fn run(&self) {
let threads = self.threads.lock();
// when run the process, the process should has only one thread
debug_assert!(threads.len() == 1);
debug_assert!(self.is_runnable());
let thread = threads[0].clone();
// should not hold the lock when run thread
drop(threads);
thread.run();
}
thread的task开始执行:
Rust
pub fn run(&self) {
self.status.lock().set_running();
self.task.run();
}
将当前task放进系统的task列表中:
Rust
pub fn add_task(task: Arc) {
GLOBAL_SCHEDULER.lock_irq_disabled().enqueue(task);
}
调度:
Rust
pub fn schedule() {
if let Some(task) = fetch_task() {
switch_to_task(task);
}
}
任务切换:
Rust
fn switch_to_task(next_task: Arc) {
if !PREEMPT_COUNT.is_preemptive() {
panic!(
“Calling schedule() while holding {} locks”,
PREEMPT_COUNT.num_locks()
);
//GLOBAL_SCHEDULER.lock_irq_disabled().enqueue(next_task);
//return;
}
let current_task_option = current_task();
let next_task_cx_ptr = &next_task.inner_ctx() as *const TaskContext;
let current_task: Arc;
let current_task_cx_ptr: *mut TaskContext = match current_task_option {
None => PROCESSOR.lock().get_idle_task_cx_ptr(),
Some(current_task) => {
if current_task.status() == TaskStatus::Runnable {
GLOBAL_SCHEDULER
.lock_irq_disabled()
.enqueue(current_task.clone());
}
&mut current_task.inner_exclusive_access().ctx as *mut TaskContext
}
};
// change the current task to the next taskPROCESSOR.lock().current = Some(next_task.clone());
unsafe {context_switch(current_task_cx_ptr, next_task_cx_ptr);
}
}
这个里面task切换的时候使用的是TaskContext,而根据前面分析可得知TaskContext中存放的是kernel_task_entry:
Rust
result.task_inner.lock().ctx.rip = kernel_task_entry as usize;
result.task_inner.lock().ctx.regs.rsp =
(crate::vm::paddr_to_vaddr(result.kstack.end_paddr())) as u64;
context_switch实现:
Rust
.text
.global context_switch
.code64
context_switch: # (cur: *mut TaskContext, nxt: *TaskContext)
Save cur’s register
mov rax, [rsp] # return address
mov [rdi + 56], rax # 56 = offsetof(Context, rip)
mov [rdi + 0], rsp
mov [rdi + 8], rbx
mov [rdi + 16], rbp
mov [rdi + 24], r12
mov [rdi + 32], r13
mov [rdi + 40], r14
mov [rdi + 48], r15
Restore nxt’s registers
mov rsp, [rsi + 0]
mov rbx, [rsi + 8]
mov rbp, [rsi + 16]
mov r12, [rsi + 24]
mov r13, [rsi + 32]
mov r14, [rsi + 40]
mov r15, [rsi + 48]
mov rax, [rsi + 56] # restore return address
mov [rsp], rax # for stack balance, must use mov instead of push
ret
在x86_64汇编中,函数调用使用的寄存器规则是:
•%rdi, %rsi, %rdx, %rcx,%r8, %r9 :六个寄存器,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9;当参数为7个以上时,前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。
•那么rdi寄存器存放的是current_task_cx_ptr,而next_task_cx_ptr存放在rsi寄存器中。
•rax寄存器被放入ctx.rip = kernel_task_entry
•执行ret指令,返回地址由rax寄存器指定。
为什么rax存放的是返回地址呢?我们看看TaskContext 的定义,从这个结构体中可以看出来结构体的rip成员变量是结构体的第7个成员。因此
mov rax, [rsi + 56] # restore return address
rsi + 56指向的是第7个成员。
Rust
pub struct CalleeRegs {
pub rsp: u64,
pub rbx: u64,
pub rbp: u64,
pub r12: u64,
pub r13: u64,
pub r14: u64,
pub r15: u64,
}
#[derive(Debug, Default, Clone, Copy)]
#[repr©]
pub(crate) struct TaskContext {
pub regs: CalleeRegs,
pub rip: usize,
}
task切换完成后,下一个task回到kernel_task_entry 处执行:
Rust
fn kernel_task_entry() {
let current_task = current_task()
.expect(“no current task, it should have current task in kernel task entry”);
current_task.func.call(());
current_task.exit();
}
kernel_task_entry调用user_task_entry:
Rust
fn user_task_entry() {
let cur = Task::current();
let user_space = cur.user_space().expect(“user task should have user space”);
let mut user_mode = UserMode::new(user_space);
debug!(
“[Task entry] rip = 0x{:x}”,
user_mode.context().instruction_pointer()
);
debug!(
“[Task entry] rsp = 0x{:x}”,
user_mode.context().stack_pointer()
);
debug!(
“[Task entry] rax = 0x{:x}”,
user_mode.context().syscall_ret()
);
loop {
let user_event: UserEvent = user_mode.execute();
let context: &mut UserContext = user_mode.context_mut();
// handle user event:
handle_user_event(user_event, context);
let current_thread = current_thread!();
// should be do this comparison before handle signal?
if current_thread.status().lock().is_exited() {
break;
}
handle_pending_signal(context).unwrap();
if current_thread.status().lock().is_exited() {
debug!(“exit due to signal”);
break;
}
// If current is suspended, wait for a signal to wake up self
while current_thread.status().lock().is_stopped() {
Thread::yield_now();
debug!(“{} is suspended.”, current_thread.tid());
handle_pending_signal(context).unwrap();
}
// a preemption point after handling user event.
preempt();
}
debug!(“exit user loop”);
// FIXME: This is a work around: exit in kernel task entry may be not called. Why this will happen?
Task::current().exit();
}
Rust
let user_event: UserEvent = user_mode.execute();
Rust
impl UserContextApiInternal for UserContext {
fn execute(&mut self) -> crate::user::UserEvent {
// set interrupt flag so that in user mode it can receive external interrupts
// set ID flag which means cpu support CPUID instruction
self.user_context.general.rflags |= (RFlags::INTERRUPT_FLAG | RFlags::ID).bits() as usize;
const SYSCALL_TRAPNUM: u16 = 0x100;// return when it is syscall or cpu exception type is Fault or Trap.loop {self.user_context.run();match CpuException::to_cpu_exception(self.user_context.trap_num as u16) {Some(exception) => {#[cfg(feature = "intel_tdx")]if *exception == VIRTUALIZATION_EXCEPTION {let ve_info =tdcall::get_veinfo().expect("#VE handler: fail to get VE info\n");handle_virtual_exception(self.general_regs_mut(), &ve_info);continue;}if exception.typ == CpuExceptionType::FaultOrTrap|| exception.typ == CpuExceptionType::Fault|| exception.typ == CpuExceptionType::Trap{break;}}None => {if self.user_context.trap_num as u16 == SYSCALL_TRAPNUM {break;}}};call_irq_callback_functions(&self.as_trap_frame());}crate::arch::irq::enable_local();if self.user_context.trap_num as u16 != SYSCALL_TRAPNUM {self.cpu_exception_info = CpuExceptionInfo {page_fault_addr: unsafe { x86::controlregs::cr2() },id: self.user_context.trap_num,error_code: self.user_context.error_code,};UserEvent::Exception} else {UserEvent::Syscall}
}
最终调用到syscall_return,UserContext作为参数,rdi寄存器指向UserContext。
Rust
.global syscall_return
syscall_return:
# disable interrupt
cli
# save callee-saved registers
mov ecx, 0xC0000100
rdmsr
shl rdx, 32
or rax, rdx
push rax # push fsbase
push r15
push r14
push r13
push r12
push rbp
push rbxpush rdi
push rdi # keep rsp 16 bytes align
mov gs:4, rsp # store kernel rsp -> TSS.sp0
mov rsp, rdi # set rsp = bottom of trap frame# pop fsbase gsbase
swapgs # store kernel gsbase
mov ecx, 0xC0000100
mov edx, [rsp + 18*8+4]
mov eax, [rsp + 18*8]
wrmsr # pop fsbase
mov ecx, 0xC0000101
mov edx, [rsp + 19*8+4]
mov eax, [rsp + 19*8]
wrmsr # pop gsbasepop rax
pop rbx
pop rcx
pop rdx
pop rsi
pop rdi
pop rbp
pop r8 # skip rsp
pop r8
pop r9
pop r10
pop r11
pop r12
pop r13
pop r14
pop r15
# rip
# rflags
# fsbase
# gsbase
# trap_num
# error_code# determain sysret or iret
cmp dword ptr [rsp + 4*8], 0x100 # syscall?
je sysret
iret:
# get user cs from STAR MSR
mov ecx, 0xC0000081
rdmsr # msr[ecx] => edx:eax
shr edx, 16 # dx = user_cs32
lea ax, [edx + 8] # ax = user_ss
add dx, 16 # dx = user_cs64
# construct trap frame
push rax # push ss
push [rsp - 8*8] # push rsp
push [rsp + 3*8] # push rflags
push rdx # push cs
push [rsp + 4*8] # push rip# recover rcx, rdx, rax
mov rax, [rsp - 11*8]
mov rcx, [rsp - 9*8]
mov rdx, [rsp - 8*8]iretq
这段代码关键在这里:
Rust
push rax # push ss
push [rsp - 88] # push rsp
push [rsp + 38] # push rflags
push rdx # push cs
push [rsp + 48] # push rip
iretq指令在执行的时候会从栈顶弹出返回地址,刚好对应:
Rust
push [rsp + 48] # push rip
而rsp值在前面被修改成了:
Rust
mov rsp, rdi # set rsp = bottom of trap frame
而rdi值是syscall_return函数调用中带进来的参数,即UserContext 变量。
Rust
pub struct UserContext {
pub general: GeneralRegs,
pub trap_num: usize,
pub error_code: usize,
}
/// General registers
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[repr©]
pub struct GeneralRegs {
pub rax: usize,
pub rbx: usize,
pub rcx: usize,
pub rdx: usize,
pub rsi: usize,
pub rdi: usize,
pub rbp: usize,
pub rsp: usize,
pub r8: usize,
pub r9: usize,
pub r10: usize,
pub r11: usize,
pub r12: usize,
pub r13: usize,
pub r14: usize,
pub r15: usize,
pub rip: usize,
pub rflags: usize,
pub fsbase: usize,
pub gsbase: usize,
}
mov rsp, rdi 想到与让rsp指向了UserContext 对象。后面一系列的pop与push操作都是在移动指针指向UserContext 对象里不同的成员,而这条指令
Rust
push [rsp + 4*8] # push rip
最终指向的是UserContext 对象里的rip成员,这个成员是被设置成应用程序的入口地址的:
Rust
cpu_ctx.set_rip(elf_load_info.entry_point() as _);
cpu_ctx.set_rsp(elf_load_info.user_stack_top() as _);
因此iretq执行的时候,从栈顶弹出的就是应用程序的入口地址,这样就跳进应用程序的入口地址进行执行了。
系统调用与返回
从上面内容研究到了程序进入入口地址开始执行程序的逻辑了,在应用程序执行的过程中可能会碰到一些系统调用,会导致程序重新陷入内核,由内核处理相关调用后再返回应用的用户空间继续执行。
首先会向系统写入一个系统调用统一的响应地址:
Rust
pub fn init() {
let cpuid = raw_cpuid::CpuId::new();
unsafe {
// enable syscall
instruction
assert!(cpuid
.get_extended_processor_and_feature_identifiers()
.unwrap()
.has_syscall_sysret());
Efer::update(|efer| {
efer.insert(EferFlags::SYSTEM_CALL_EXTENSIONS);
});
// flags to clear on syscall// copy from Linux 5.0// TF|DF|IF|IOPL|AC|NTconst RFLAGS_MASK: u64 = 0x47700;LStar::write(VirtAddr::new(syscall_entry as usize as u64));SFMask::write(RFlags::from_bits(RFLAGS_MASK).unwrap());
}
}
syscall_entry会作为系统调用的响应入口,也就是说当应用程序调用系统调用的时候,系统会捕捉到系统调用请求,然后就请求转到syscall_entry处理。
Rust
.global syscall_entry
syscall_entry:
# syscall instruction do:
# - load cs
# - store rflags -> r11
# - mask rflags
# - store rip -> rcx
# - load rip
swapgs # swap in kernel gs
mov gs:12, rsp # store user rsp -> scratch at TSS.sp1
mov rsp, gs:4 # load kernel rsp <- TSS.sp0
pop rsp # load rsp = bottom of trap frame
add rsp, 22*8 # rsp = top of trap frame# push trap_num, error_code
push 0 # push error_code
push 0x100 # push trap_num
sub rsp, 16 # skip fsbase, gsbase
# push general registers
push r11 # push rflags
push rcx # push rip
.global trap_syscall_entry
trap_syscall_entry:
push r15
push r14
push r13
push r12
push r11
push r10
push r9
push r8
push gs:12 # push rsp
push rbp
push rdi
push rsi
push rdx
push rcx
push rbx
push rax
# push fsbase gsbase
mov ecx, 0xC0000100
rdmsr
mov [rsp + 18*8+4], edx
mov [rsp + 18*8], eax
mov ecx, 0xC0000102 # kernelgs
rdmsr
mov [rsp + 19*8+4], edx
mov [rsp + 19*8], eax# restore callee-saved registers
mov rsp, gs:4 # load kernel rsp <- TSS.sp0
pop rbx
pop rbx
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15pop rax
mov ecx, 0xC0000100
mov rdx, rax
shr rdx, 32
wrmsr # pop fsbase# go back to Rust
ret
这段代码的关键点在于:
Rust
mov rsp, gs:4 # load kernel rsp <- TSS.sp0
将kernel的栈顶放置进rsp。
而在syscall_return调用的时候:
Rust
mov gs:4, rsp # store kernel rsp -> TSS.sp0
会将kernel 栈顶保存进gs:4。
因此当kernel rsp被回复后,会进行一系列的弹栈操作:
Rust
# restore callee-saved registers
mov rsp, gs:4 # load kernel rsp <- TSS.sp0
pop rbx
pop rbx
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
pop rax
这个跟syscall_return调用时候的压栈操作一一对应,这样弹完栈后栈顶会指向函数的返回地址,即syscall_return调用的返回地址,这样执行ret指令后程序会返回到内核里调用syscall_return处,继续处理相关的系统调用。
所以从这个角度来看整个task的切换:
Task1.kernel_task_entry-> Task1.user_task_entry -> 通过retq/sysretq返回到用户空间-> syscall_entry进入内核->Task1.user_task_entry处理系统调用之类的。
任务调度
在asterinas系统中构建有一个基于优先级的可抢占的调度器:
Rust
pub fn init() {
let preempt_scheduler = Box::new(PreemptScheduler::new());
let scheduler = Box::::leak(preempt_scheduler);
set_scheduler(scheduler);
}
该调度器将任务分成两种类型,实时任务与普通任务:
Rust
struct PreemptScheduler {
/// Tasks with a priority of less than 100 are regarded as real-time tasks.
real_time_tasks: SpinLock<LinkedList>,
/// Tasks with a priority greater than or equal to 100 are regarded as normal tasks.
normal_tasks: SpinLock<LinkedList>,
}
任务抢占,在任务的user_task_entry中会有一个抢占点:
Rust
pub fn create_new_user_task(user_space: Arc, thread_ref: Weak) -> Arc {
fn user_task_entry() {
let cur = Task::current();
let user_space = cur.user_space().expect(“user task should have user space”);
let mut user_mode = UserMode::new(user_space);
debug!(
“[Task entry] rip = 0x{:x}”,
user_mode.context().instruction_pointer()
);
debug!(
“[Task entry] rsp = 0x{:x}”,
user_mode.context().stack_pointer()
);
debug!(
“[Task entry] rax = 0x{:x}”,
user_mode.context().syscall_ret()
);
loop {
let user_event = user_mode.execute();
let context = user_mode.context_mut();
// handle user event:
handle_user_event(user_event, context);
let current_thread = current_thread!();
// should be do this comparison before handle signal?
if current_thread.status().lock().is_exited() {
break;
}
handle_pending_signal(context).unwrap();
if current_thread.status().lock().is_exited() {
debug!(“exit due to signal”);
break;
}
// If current is suspended, wait for a signal to wake up self
while current_thread.status().lock().is_stopped() {
Thread::yield_now();
debug!(“{} is suspended.”, current_thread.tid());
handle_pending_signal(context).unwrap();
}
// a preemption point after handling user event.
preempt();
}
debug!(“exit user loop”);
// FIXME: This is a work around: exit in kernel task entry may be not called. Why this will happen?
Task::current().exit();
}
该抢占点会去检查当前任务是否能被其他任务抢占:
Rust
pub fn preempt() {
// disable interrupts to avoid nested preemption.
let disable_irq = disable_local();
let Some(curr_task) = current_task() else {
return;
};
let mut scheduler = GLOBAL_SCHEDULER.lock_irq_disabled();
if !scheduler.should_preempt(&curr_task) {
return;
}
let Some(next_task) = scheduler.dequeue() else {
return;
};
drop(scheduler);
switch_to_task(next_task);
}
should_preempt逻辑很简单,如果当前任务不是实时任务,且待执行的实时任务列表不为空则代表当前任务可以被抢占
Rust
fn should_preempt(&self, task: &Arc) -> bool {
!task.is_real_time() && !self.real_time_tasks.lock_irq_disabled().is_empty()
}
如果能被抢占,则调度一个新任务开始执行:
Rust
let Some(next_task) = scheduler.dequeue() else {
return;
};
drop(scheduler);
switch_to_task(next_task);
所以我们上面描述的简单逻辑可以是这样的:
Task1.kernel_task_entry-> Task1.user_task_entry -> 通过retq/sysretq返回到用户空间-> syscall_entry进入内核->Task1.user_task_entry处理系统调用之类的->能被抢占->switch_to_task ->Task2.kernel_task_entry->Task2.user_task_entry -> 通过retq/sysretq返回到用户空间-> syscall_entry进入内核->Task2.user_task_entry处理系统调用之类的
大概类似这样的流程。
几个不完善的点:
•感觉貌似对多CPU,多处理核心没有支持。
•另外抢占不是实时发生的,是必须等到正在执行的任务处理完成到某个阶段才发生。而且必须是当前任务陷入进kernel的时候才能被抢占。
相关文章:
![](https://www.ngui.cc/images/no-images.jpg)
【Asterinas】Asterinas 进程启动与切换
Asterinas 进程启动与切换 进程启动 进程创建: Rust pub fn spawn_user_process( executable_path: &str, argv: Vec, envp: Vec, ) -> Result<Arc> { // spawn user process should give an absolute path debug_assert!(executable_path.starts_with…...
![](https://img-blog.csdnimg.cn/img_convert/327df6dfff26faeaeead0f93c692296d.png)
CVE-2024-6387 分析
文章目录 1. 漏洞成因2. 漏洞利用前置知识2.1 相关 SSH 协议报文格式2.2 Glibc 内存分配相关规则 3. POC3.1 堆内存布局3.2 服务端解析数据时间测量3.3 条件竞争3.4 FSOP 4. 相关挑战 原文链接:个人博客 近几天,OpenSSH爆出了一个非常严重的安全漏洞&am…...
![](https://img-blog.csdnimg.cn/direct/af70d5f0ac3b40a9b89f5636069129d6.png)
STM32 ADC精度提升方法
STM32 ADC精度提升方法 Fang XS.1452512966qq.com如果有错误,希望被指出,学习技术的路难免会磕磕绊绊量的积累引起质的变化 硬件方法 优化布局布线,尽量减小其他干扰增加电源、Vref去耦电容使用低通滤波器,或加磁珠使用DCDC时尽…...
![](https://img-blog.csdnimg.cn/direct/2fb462d497fd466dac033f3b11bc00f0.png)
Redis为什么设计多个数据库
关于Redis的知识前面已经介绍过很多了,但有个点没有讲,那就是一个Redis的实例并不是只有一个数据库,一般情况下,默认是Databases 0。 一 内部结构 设计如下: Redis 的源码中定义了 redisDb 结构体来表示单个数据库。这个结构有若干重要字段,比如: dict:该字段存储了…...
![](https://img-blog.csdnimg.cn/direct/8fe70aee0e52491da56c2bb0bcac0df4.png)
零基础学习MySQL---MySQL入门
顾得泉:个人主页 个人专栏:《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂,年薪百万! 一、什么是数据库 问:存储数据用文件就可以了,为什么还要弄个数据库呢? 这就不得不提…...
![](https://img-blog.csdnimg.cn/direct/c8b060f5e4bc4e449ed05a36a02e0211.png)
HUAWEI MPLS 静态配置和动态LDP配置
MPLS(Multi-Protocol Label Switching,多协议标签交换技术)技术的出现,极大地推动了互联网的发展和应用。例如:利用MPLS技术,可以有效而灵活地部署VPN(Virtual Private Network,虚拟专用网),TE(Traffic Eng…...
![](https://www.ngui.cc/images/no-images.jpg)
【Rust】——所有的模式语法
💻博主现有专栏: C51单片机(STC89C516),c语言,c,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux…...
![](https://img-blog.csdnimg.cn/direct/5b1d6de83c224a0ea77f7750a8090abd.png)
基于Python的求职招聘管理系统【附源码】
摘 要 随着互联网技术的不断发展,人类的生活已经逐渐离不开网络了,在未来的社会中,人类的生活与工作都离不开数字化、网络化、电子化与虚拟化的数字技术。从互联网的发展历史、当前的应用现状和发展趋势来看,我们完全可以肯定&…...
![](https://img-blog.csdnimg.cn/img_convert/b7e3f21a73c8310ac55f5032bb33b2ff.png)
Python23 使用Tensorflow实现线性回归
TensorFlow 是一个开源的软件库,用于数值计算,特别适用于大规模的机器学习。它由 Google 的研究人员和工程师在 Google Brain 团队内部开发,并在 2015 年首次发布。TensorFlow 的核心是使用数据流图来组织计算,使得它可以轻松地利…...
![](https://www.ngui.cc/images/no-images.jpg)
C++:枚举类的使用案例及场景
一、使用案例 在C中,枚举类(也称为枚举类型或enum class)是C11及以后版本中引入的一种更加强大的枚举类型。与传统的枚举(enum)相比,枚举类提供了更好的类型安全性和作用域控制。下面是一个使用枚举类的案…...
![](https://www.ngui.cc/images/no-images.jpg)
中英双语介绍美国的州:明尼苏达州(Minnesota)
中文版 明尼苏达州简介 明尼苏达州位于美国中北部,以其万湖之州的美誉、丰富的自然资源和多样化的经济结构而著称。以下是对明尼苏达州的详细介绍,包括其地理位置、人口、经济、教育、文化和主要城市。 地理位置 明尼苏达州东接威斯康星州࿰…...
![](https://img-blog.csdnimg.cn/direct/d4ef97dd55da4e8ea07b01e951606b57.png)
Python实现万花筒效果:创造炫目的动态图案
文章目录 引言准备工作前置条件 代码实现与解析导入必要的库初始化Pygame定义绘制万花筒图案的函数主循环 完整代码 引言 万花筒效果通过反射和旋转图案创造出美丽的对称图案。在这篇博客中,我们将使用Python来实现一个动态的万花筒效果。通过利用Pygame库…...
![](https://img-blog.csdnimg.cn/direct/2ed7db66b56c4e9fb165e8af20d301aa.gif)
JavaScript之深入对象,详细讲讲构造函数与常见内置构造函数
前言:哈喽,大家好,我是前端菜鸟的自我修养!今天给大家详细讲讲构造函数与常见内置构造函数,并提供具体代码帮助大家深入理解,彻底掌握!原创不易,如果能帮助到带大家,欢迎…...
![](https://img-blog.csdnimg.cn/direct/bc47d6719c904886b8adac924240d61c.png)
PyQt5水平布局--只需5分钟带你搞懂
PyQt5水平布局(QHBoxLayout)是一种在GUI应用程序中用于组织和排列控件的布局方式。它允许开发者将控件在水平方向上从左到右依次排列,非常适合于需要并排显示控件的场景,如工具栏、水平菜单等。 import sys from PyQt5.QtWidgets…...
![](https://www.ngui.cc/images/no-images.jpg)
telegram mini app和game实现登录功能
接上一篇文章,我们在创建好telegram机器人后,开始开发小游戏或者mini App,那就避免不了登录功能。 公开链接 bot设置教程:https://lengmo714.top/6e79860b.html 参考教程参考教程,telegram已经给我们提供非常多的api,我们在获取用…...
![](https://img-blog.csdnimg.cn/direct/6b7ca049e8164bfca992beddc804e85e.png)
【Python】字典练习
python期考练习 目录 1. 首都名编辑 2. 摩斯电码 3. 登录 4. 学生的姓名和年龄编辑 5. 电商 6. 学生基本信息 7. 字母数 1. 首都名 初始字典 (可复制) : d{"China":"Beijing","America":"Washington","Norway":…...
![](https://img-blog.csdnimg.cn/direct/36eda21e344546faaf96d59236c867c3.png)
Apache POI、EasyPoi、EasyExcel
目录 编辑 (一)Apache PoI 使用 (二)EasyPoi使用 (三)EasyExcel使用 写 读 最简单的读 最简单的读的excel示例 最简单的读的对象 (一)Apache PoI 使用 (二&…...
![](https://www.ngui.cc/images/no-images.jpg)
gcop:简化 Git 提交流程的高效助手 | 一键生成 commit message
💖 大家好,我是Zeeland。Tags: 大模型创业、LangChain Top Contributor、算法工程师、Promptulate founder、Python开发者。📣 个人说明书:Zeeland📣 个人网站:https://me.zeeland.cn/📚 Github…...
![](https://img-blog.csdnimg.cn/direct/30538b25289141f3a92c440dd077e5b9.gif)
TS_类型
目录 1.类型注解 2.类型检查 3.类型推断 4.类型断言 ①尖括号(<>)语法 ②as语法 5.数据类型 ①boolean ②number ③string ④undefined 和 null ⑤数组和元组 ⑥枚举 ⑦any 和void ⑧symbol ⑨Function ⑩Object 和 object 6.高…...
![](https://img-blog.csdnimg.cn/img_convert/83f4e1341435c6a98f64579641e748ff.png)
Linux源码阅读笔记10-进程NICE案例分析2
set_user_nice set_user_nice函数功能:设置某一进程的NICE值,其NICE值的计算是根据进程的静态优先级(task_struct->static_prio),直接通过set_user_nice函数更改进程的静态优先级。 内核源码 void set_user_nice…...
![](https://img-blog.csdnimg.cn/direct/f76017beafe74c1f9628343ab3a72a33.png)
Elasticsearch实战教程: 如何在海量级数据中进行快速搜索
🎬 鸽芷咕:个人主页 🔥 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 引入 Elasticsearch(简称ES)是一个基于Apache Lucene™的开源搜索引擎,无论在开源还是专有领…...
![](https://img-blog.csdnimg.cn/direct/6553eef55edc424ead9df1585240305c.png)
Python学习笔记24:进阶篇(十三)常见标准库使用之数据压缩功能模块zlib,gzip,bz2,lzma的学习使用
前言 本文是根据python官方教程中标准库模块的介绍,自己查询资料并整理,编写代码示例做出的学习笔记。 根据模块知识,一次讲解单个或者多个模块的内容。 教程链接:https://docs.python.org/zh-cn/3/tutorial/index.html 数据压缩…...
![](https://www.ngui.cc/images/no-images.jpg)
【笔记】Android Settings 应用设置菜单的界面代码介绍
简介 Settings应用中,提供多类设置菜单入口,每个菜单内又有各模块功能的实现。 那么各个模块基于Settings 基础的界面Fragment去实现UI,层层按不同业务进行封装继承实现子类: DashboardFragmentSettingsPreferenceFragment 功…...
![](https://www.ngui.cc/images/no-images.jpg)
Symfony配置管理深度解析:构建可维护项目的秘诀
Symfony是一个高度灵活且功能丰富的PHP框架,它提供了一套强大的配置管理系统,使得开发者能够轻松定制和优化应用程序的行为。本文将深入探讨Symfony中的配置管理机制,包括配置的结构、来源、加载过程以及最佳实践。 一、配置管理的重要性 在…...
![](https://img-blog.csdnimg.cn/img_convert/0ea61f97e0d8c149def6130b912e5a79.png)
视频的宣传片二维码怎么做?扫码播放视频的制作教程
现在很多的宣传片会通过扫码的方式来展示,通过将视频生成二维码之后,其他人就可以扫码来查看视频内容,从而简化获取视频的过程,提升视频传播的效率及用户查看视频的便捷性。目前,日常生活和工作中就有视频二维码的应用…...
![](https://www.ngui.cc/images/no-images.jpg)
实用的网站
前端 精简CSS格式 Font Awesome 图标库 BootCDN 加速服务 LOGO U钙网 AI AI工具集 视频下载 B站视频解析下载...
![](https://img-blog.csdnimg.cn/direct/ccd25a05241c487bb6d608bdc4d6b202.png)
Monorepo(单体仓库)与 MultiRepo(多仓库): Monorepo 单体仓库开发策略与实践指南
🔥 个人主页:空白诗 文章目录 一、引言1. Monorepo 和 MultiRepo 简介2. 为什么选择 Monorepo? 二、Monorepo 和 MultiRepo 的区别1. 定义和概述2. 各自的优点和缺点3. 适用场景 三、Monorepo 的开发策略1. 版本控制2. 依赖管理3. 构建和发布…...
![](https://img-blog.csdnimg.cn/img_convert/aa6c051c59be164cee0f3784d076228e.jpeg)
使用 PyTorch 创建的多步时间序列预测的 Encoder-Decoder 模型
Encoder-decoder 模型在序列到序列的自然语言处理任务(如语言翻译等)中提供了最先进的结果。多步时间序列预测也可以被视为一个 seq2seq 任务,可以使用 encoder-decoder 模型来处理。本文提供了一个用于解决 Kaggle 时间序列预测任务的 encod…...
![](https://www.ngui.cc/images/no-images.jpg)
开启IT世界的第一步:高考新生的暑期学习指南
目录 前言 了解IT领域 学习编程语言 实践项目 学习资源 阅读专业书籍 培养良好的学习习惯 结语 最后 - 投票 前言 七月的钟声敲响,各省的高考分数已揭晓,意味着一段紧张而又充满奋斗的旅程画上了句号。然而,高考的结束并不意味…...
![](https://img-blog.csdnimg.cn/img_convert/5970927918c775954ddcc9237a1081a8.png)
软考系统架构师高效备考方法论
软考系统架构师高效备考方法论 本章总结的备考方法论也是希望能帮助更多的小伙伴高效的备考最终通过考试,这种考试个人感觉是尽量一次性考过, 要不然老拖着,虽然每年可以考两次,5月和11月,两次考试间隔5个月时间&#…...
![](https://img-blog.csdnimg.cn/direct/2a7c3e4f02394e39973b18cd1e67300f.png)
【neo4j图数据库】入门实践篇
探索数据之间的奥秘:Neo4j图数据库引领新纪元 在数字化浪潮汹涌的今天,数据已成为企业最宝贵的资产之一。然而,随着数据量的爆炸性增长和数据关系的日益复杂,传统的关系型数据库在处理诸如社交网络、推荐系统、生物信息学等高度互…...
![](https://img-blog.csdnimg.cn/cc002cbd5c414c5393e19c5e0a0dbf20.gif#pic_center#pic_center)
【TS】TypeScript 原始数据类型深度解析
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 TypeScript 原始数据类型深度解析一、引言二、基础原始数据类型2.1 boolean2.2 …...
![](https://www.ngui.cc/images/no-images.jpg)
怎么样调整分类的阈值
调整分类模型的阈值是改变模型对正负类的预测标准的一种方法,常用于提高精确率、召回率或者其他性能指标。以下是如何调整分类阈值的步骤和方法: PS:阈值是针对预测概率(表示样本属于某个特定类别的可能性)来说的 调…...
![](https://img-blog.csdnimg.cn/direct/96f835aadeed41b69b057194b643283c.jpeg)
java+mysql教师管理系统
完整源码地址 教师信息管理系统使用命令行交互的方式及数据库连接实现教师信息管理系统,该系统旨在实现教师信息的管理,并根据需要进行教师信息展示。该软件的功能有如下功能 (1)基本信息管理(教师号、姓名、性别、出生年月、职称、学历、学位、教师类型…...
![](https://img-blog.csdnimg.cn/img_convert/5f5c983a9df2f800530200ff51f92fd9.png)
PDF文档如何统计字数,统计PDF文档字数的方法有哪些?
在平时使用pdf阅读或者是处理文档的时候,常常需要统计文档的字数。pdf在查看文字时其实很简单。 PDF文档是一种常见的电子文档格式,如果需要对PDF文档中的字数进行统计,可以使用以下方法: Adobe Acrobat DC:Adobe Ac…...
![](https://www.ngui.cc/images/no-images.jpg)
在Python asyncio中如何识别协程是否被block了
现在asyncio在Python中的使用越来越广泛了,但是很多人对于协程(corotine)的一些使用方式还不太熟悉。在这篇文章中,我将会介绍如何识别协程是否被block了,并以常用的HTTP网络库requests/httpx为例来说明如何避免协程被block的问题。 为什么协程会被block 在Python中,可…...
![](https://img-blog.csdnimg.cn/direct/9873f2cce9eb4102b48a2539cc6765ca.png)
Hyper-V虚拟机固定IP地址(手把手教设置)
链接虚拟机修改网络配置文件 输入指令 sudo vi /etc/sysconfig/network-scripts/ifcfg-eth0 然后 输入 按 i 键 再按回车 (enter) 进入编辑模式 修改配置(这几项)其中 IPADDR 就是你想给虚拟机固定的 IP 地址 多台的话只需要修改这个IP 就行其他不变 BOOTPROTO=static…...
![](https://img-blog.csdnimg.cn/img_convert/2f0817607f02f1fd6e012ffdf3884840.jpeg)
以 Vue 3 项目为例,多个请求下如何全局封装 Loading 的展示与关闭?其中大有学问!
大家好,我是CodeQi! 项目开发中,Loading 的展示与关闭是非常关键的用户体验设计。 当我们的应用需要发起多个异步请求时,如何有效地管理全局 Loading 状态,保证用户在等待数据加载时能有明确的反馈,这是一个值得深入探讨的问题。 本文将以 Vue 3 项目为例,详细讲解如…...
![](https://img-blog.csdnimg.cn/direct/079b86647c8e4fd9938e00de19ae5a0d.png)
Node.js学习(一)
Node.js安装与入门案例: 需求:点击按钮,请求本地目录指定文件的内容,并显示在页面上 刚入门肯定想着直接写相对路径请求指定路径数据就行了,可是会发现不行。 网页运行在浏览器端,通常后续要发布…...
![](https://www.ngui.cc/images/no-images.jpg)
Spring Data JPA使用及实现原理总结
Spring Data JPA系列 1、SpringBoot集成JPA及基本使用 2、Spring Data JPA Criteria查询、部分字段查询 3、Spring Data JPA数据批量插入、批量更新真的用对了吗 4、Spring Data JPA的一对一、LazyInitializationException异常、一对多、多对多操作 5、Spring Data JPA自定…...
![](https://img-blog.csdnimg.cn/direct/3c77c93d11c0478fbad101addeb45347.png#pic_center)
【C语言】extern 关键字
在C语言中,extern关键字用于声明一个变量或函数是定义在另一个文件中的。它使得在多个文件之间共享变量或函数成为可能。extern关键字常见于大型项目中,通常用于声明全局变量或函数,这些变量或函数的定义位于其他文件中。 基本用法 变量声明…...
![](https://www.ngui.cc/images/no-images.jpg)
Linux--V4L2应用程序开发(二)改变亮度
一、思路流程 创建一个新线程用来控制亮度,线程通过读取用户输入来增加或减少亮度值,并使用 ioctl 函数将新亮度值设置到视频设备。 二、代码 /*创建线程来控制亮度*/ pthread_t thread; pthread_create(&thread, NULL, thread_brightness_contrl…...
![](https://www.ngui.cc/images/no-images.jpg)
[Gstreamer] 消息处理handler的设置
前言: Gstreamer 提供以 GstMessage 和 GstBus 为基础的消息传递机制,所有GstMessage 发送的时候都需要指定 GstBus 用来明确当前 message 将在哪条 Bus 上流转。所有的 GstMessage 最终都会进入一个handler,这个handler函数可以通过两种方式…...
![](https://img-blog.csdnimg.cn/direct/0bd0b66b30cb4ffa877e30a9fb32507b.png)
线性代数笔记
行列式 求高阶行列式 可以划上三角 上三角 余子式 范德蒙行列式 拉普拉斯公式 行列式行列对换值不变 矩阵 矩阵的运算 同型矩阵加减 对应位置相加减 矩阵的乘法 左边第 i 行 一次 相乘求和 右边 第 j 列 eg 中间相等 两边规模 矩阵的幂运算 解题思路 找规律 数学归纳…...
![](https://img-blog.csdnimg.cn/direct/e2d7f3c4ff54412f8b58fae2c31c408f.jpeg#pic_center)
未公开 GeoServer开源服务器wfs远程命令执行漏洞 已复现(CVE-2024-36401)
0x01 阅读须知 技术文章仅供参考,此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成…...
![](https://img-blog.csdnimg.cn/direct/f4448d8c09d843008e99c39d493a85dc.png)
【WebGIS干货分享】Webgis 面试题-浙江中海达
1、Cesium 中有几种拾取坐标的方式,分别介绍 Cesium 是一个用于创建 3D 地球和地理空间应用的 JavaScript 库。在 Cesium 中,你可以使用不同的方式来拾取坐标,以便与地球或地图上的对象进行交 互。以下是 Cesium 中几种常见的拾取坐标的方式…...
![](https://www.ngui.cc/images/no-images.jpg)
ES 修改索引字段类型
大体的原理: 1:按照老索引按需修改,新建新索引 2:转移数据(数据量大,时间会很长) 3:删除老索引 4:给新索引 创建别名 第一步:创建新索引 可以先获取老索引ma…...
![](https://img-blog.csdnimg.cn/direct/989ecaf30274491296c7af00e878ce25.png)
恢复的实现技术-日志和数据转储
一、引言 在系统正常运行的情况下,事务处理的恢复机制应采取某些技术措施为恢复做好相应的准备,保证在系统发生故障后,能将数据库从一个不一致的错误状态恢复到一个一致性状态 恢复技术主要包括 生成一个数据库日志,来记录系统中…...
![](https://img-blog.csdnimg.cn/direct/7294f6e766b04e0193acf411b335a0c9.png)
全网最全最细的jmeter接口测试教程,建议收藏
在日常工作中,尤其是做接口测试时,我们最经常用到的两个工具,就是Jmeter和postman。今天,我们主要是讲一讲Jmeter在接口测试这一块的一些方式方法。内容比较多,大家可以收藏一下,以后慢慢学。 1࿰…...
![](https://www.ngui.cc/images/no-images.jpg)
Raspbian命令行连接WiFi网络
Raspbian命令行连接WiFi网络 1. 源由2. 环境3. 信号4. 连接5. 检查6. 断开 1. 源由 “懒人”多福,是什么原因,大家知道不,哈哈。 如果大家关注过之前《Ardupilot开源代码之Rover上路计划》,为了笔记本电脑在不断网的情况下进行配…...
![](https://www.ngui.cc/images/no-images.jpg)
flask-apscheduler 定时任务被执行两次
定时任务没有被多次调用,事实上如果多次调用的话,flask-apscheduler会抛出异常。 但一开始任务还是按时执行一次,重启flask或者修改部分代码后,就发送定时任务一次执行两次了。 之所以会发生这种情况,是因为在debug模…...
![](https://www.ngui.cc/images/no-images.jpg)
Excel 中的元素定位:相对定位、绝对定位和混合定位
在Excel中,单元格引用有三种主要类型:相对定位、绝对定位和混合定位。 这些类型主要用于公式和函数中,决定在复制或拖动公式时引用如何变化。 1. 相对定位 相对定位指的是不带“$”符号的单元格引用,例如 A1。 这种引用方式在…...
![](https://img-blog.csdnimg.cn/img_convert/65410a708fd9732f4f8b40ffca5c98f5.jpeg)
北京十大拆迁律师事务所排名
历史时刻在重演,土地征地拆迁作为城市发展中不可或缺的环节备受地方政府重视。然而,在土地征收过程中,往往因为拆迁补偿引发各种纠纷案件,给拆迁方和被拆迁方带来重大损失,侵害双方利益,尤其是被征收人。因…...
![](https://img-blog.csdnimg.cn/direct/074b23e5b81f4fd2a10455f51f47d7f5.png)
docker -run hello-world超时
主要原因就是尝试拉取库的时候没有从阿里云镜像里拉,所以设置一下就好了 这里使用的是ubuntu系统(命令行下逐行敲就行了) sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": [&quo…...
![](https://www.ngui.cc/images/no-images.jpg)
墨烯的C语言技术栈-C语言基础-006
六.常量 C语言的常量为 字面常量 const修饰的常变量 #define定义的 枚举常量 int main() { // 四个都是字面常量 30; 3.14; "w"; // 字符常量 "abc"; // const修饰的常变量 const int a 10; // 在C语言中,const修饰的a,本质是变量,但不能直…...
![](https://img-blog.csdnimg.cn/direct/236ba1e170534052a828d0b2778adf49.png)
TF-IDF计算过程一步步推导详解含代码演示
相关概念 TF-IDF TF-IDF(Term Frequency–Inverse Document Frequency)是一种用于资讯检索与文本挖掘的常用加权技术。TF-IDF是一种统计方法,用以评估一个字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在…...
![](https://www.ngui.cc/images/no-images.jpg)
绿色生态发展底色如何落地“生”金?且看林下经济的“江西实践”
何为“林下经济”?林下经济是以林地资源为依托,以科技为支撑,充分利用林下土地资源和林荫空间,选择适合林下生长的微生物和动植物种类,进行合理种植、养殖,从而为农村经济提供新的增长点、为农民开辟增收致富新路子的新兴生态农业模式。江西省是全国首个“国家森林城市”…...
![](https://img-blog.csdnimg.cn/direct/5e15a54a92af472ba05480991120acd5.gif#pic_center)
【产品经理】技术知识
引言: 在最近频繁的产品管理职位面试中,我深刻体会到了作为产品经理需要的不仅仅是对市场和技术的敏锐洞察,更多的是在复杂多变的环境中,如何运用沟通、领导力和决策能力来引导产品从概念走向市场。这一系列博客将分享…...
![](https://img-blog.csdnimg.cn/direct/29ff0cc4046141789b0dd4cf8ed363e5.png)
The Sandbox 和 Bitkub 联手增强东南亚元宇宙中心
作为去中心化游戏虚拟世界和区块链平台的先驱,The Sandbox 正与泰国领先的区块链网络 Bitkub Blockchain Technology Co., Ltd. 展开创新合作。双方合作的目的是将Bitkub元宇宙的影响力扩展到The Sandbox,建立一个元宇宙中心,向用户承诺从 Bi…...
![](https://www.ngui.cc/images/no-images.jpg)
使用 Python 发送 TRON-USDT 和 TRX 交易
文章目录 一、前提准备1.1 环境设置1.2 获取钱包地址和私钥二、发送USDT(TRC-20)交易2.1 流程2.2 USDT交易脚本三、发送TRX交易3.1 流程3.2 TRX交易脚本四、注意事项4.1 注意事项4.2 改进后的USDT和TRX交易脚本本文档介绍如何使用Python和tronpy库发送TRON网络上的USDT(TRC-…...
![](https://www.ngui.cc/images/no-images.jpg)
k8s cephfs(动态pvc)
官方参考文档:GitHub - ceph/ceph-csi at v3.9.0 测试版本 Ceph Version Ceph CSI Version Container Orchestrator Name Version Tested v17.2.7 v3.9.0 Kubernetes v1.25.6 安装Ceph-csi Step 1 Download GitHub - ceph/ceph-csi at v3.9.0 rootsd-k8s…...
![](https://img-blog.csdnimg.cn/6ab6307743f44d209a6e289abcde571a.png)
智慧城市运维可视化:透视未来城市高效管理的新视窗
行业痛点 现代城市运维是一个复杂而庞大的系统,涉及到诸多方面,包括交通、环境、能源等等。然而,在城市运维中,存在着一些现实的痛点,给城市管理者带来了不小的压力和困扰: 1、交通拥堵 随着城市化进程的…...