当前位置: 首页 > news >正文

Linux中内核调用用户空间程序的实现

从内核空间启动用户空间程序call_usermodehelper

int call_usermodehelper(char *path, char **argv, char **envp, int wait)
{DECLARE_COMPLETION(done);struct subprocess_info sub_info = {.complete       = &done,.path           = path,.argv           = argv,.envp           = envp,.wait           = wait,.retval         = 0,};DECLARE_WORK(work, __call_usermodehelper, &sub_info);if (!khelper_wq)return -EBUSY;if (path[0] == '\0')return 0;queue_work(khelper_wq, &work);wait_for_completion(&done);return sub_info.retval;
}
void __init usermodehelper_init(void)
{khelper_wq = create_singlethread_workqueue("khelper");BUG_ON(!khelper_wq);
}

函数功能

call_usermodehelper 用于从内核空间启动用户空间程序,是内核与用户空间交互的重要机制

代码逐段解释

call_usermodehelper 函数

第1-2行:函数声明和完成量定义
int call_usermodehelper(char *path, char **argv, char **envp, int wait)
{DECLARE_COMPLETION(done);
  • 参数
    • path: 要执行的用户空间程序路径
    • argv: 参数数组(以NULL结束)
    • envp: 环境变量数组(以NULL结束)
    • wait: 是否等待程序执行完成
  • DECLARE_COMPLETION(done):
    • 声明并初始化一个完成量(completion)
    • 用于同步,当工作完成后会通知等待者
第3-10行:子进程信息结构体初始化
        struct subprocess_info sub_info = {.complete       = &done,.path           = path,.argv           = argv,.envp           = envp,.wait           = wait,.retval         = 0,};
  • struct subprocess_info: 包含执行用户空间程序所需的所有信息
  • 字段说明
    • .complete = &done: 指向完成量,用于通知执行完成
    • .path = path: 要执行的程序路径
    • .argv = argv: 程序参数
    • .envp = envp: 环境变量
    • .wait = wait: 是否等待标志
    • .retval = 0: 返回值初始化为0
第11行:工作项声明
        DECLARE_WORK(work, __call_usermodehelper, &sub_info);
  • DECLARE_WORK: 声明一个工作队列项
  • 参数
    • work: 工作项名称
    • __call_usermodehelper: 实际执行工作的函数
    • &sub_info: 传递给工作函数的参数
第13-14行:工作队列检查
        if (!khelper_wq)return -EBUSY;
  • khelper_wq: 全局工作队列指针
  • 检查工作队列是否已初始化,如果没有则返回-EBUSY(系统忙)
第16-17行:空路径检查
        if (path[0] == '\0')return 0;
  • 检查路径是否为空字符串
  • 如果是空路径,直接返回0(成功),不执行任何操作
第19行:提交工作到工作队列
        queue_work(khelper_wq, &work);
  • queue_work: 将工作项提交到工作队列
  • khelper_wq: 内核工作队列
  • &work: 要执行的工作项
第20-21行:等待完成并返回结果
        wait_for_completion(&done);return sub_info.retval;
  • wait_for_completion(&done):
    • 等待完成量,阻塞当前线程直到工作完成
    • __call_usermodehelper执行完成后会触发完成量
  • return sub_info.retval: 返回子进程的执行结果

usermodehelper_init 函数

第1行:初始化函数声明
void __init usermodehelper_init(void)
  • __init: 表示该函数只在初始化阶段使用,初始化完成后内存会被释放
  • 系统启动时调用的初始化函数
第2-3行:创建工作队列
        khelper_wq = create_singlethread_workqueue("khelper");BUG_ON(!khelper_wq);
  • create_singlethread_workqueue("khelper"):
    • 创建单线程的工作队列
    • "khelper"是工作队列的名称
    • 返回工作队列指针赋值给全局变量khelper_wq
  • BUG_ON(!khelper_wq):
    • 如果创建工作队列失败(返回NULL),触发内核BUG
    • 这是严重错误,系统无法正常运行

创建内核线程来启动用户空间程序__call_usermodehelper

static void __call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;/* CLONE_VFORK: wait until the usermode helper has execve'd* successfully We need the data structures to stay around* until that is done.  */if (sub_info->wait)pid = kernel_thread(wait_for_helper, sub_info,CLONE_FS | CLONE_FILES | SIGCHLD);elsepid = kernel_thread(____call_usermodehelper, sub_info,CLONE_VFORK | SIGCHLD);if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);} else if (!sub_info->wait)complete(sub_info->complete);
}

函数功能

这个函数是用户模式辅助函数,负责创建内核线程来启动用户空间程序

代码逐段解释

第1-3行:函数声明和变量定义

static void __call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;
  • static void: 静态函数,只在当前文件内可见
  • void *data: 泛型指针参数,实际指向struct subprocess_info
  • struct subprocess_info *sub_info = data:
    • 将泛型指针转换为具体的结构体指针
    • 包含执行用户空间程序所需的所有信息
  • pid_t pid: 存储新创建的内核线程的进程ID

第5-8行:注释说明克隆标志的含义

        /* CLONE_VFORK: wait until the usermode helper has execve'd* successfully We need the data structures to stay around* until that is done.  */
  • CLONE_VFORK: 重要的克隆标志
  • 作用: 父线程(内核线程)会等待子线程成功执行execve()系统调用
  • 必要性: 确保子进程在执行execve()之前,父进程的数据结构保持有效

第9-14行:根据等待标志选择执行策略

        if (sub_info->wait)pid = kernel_thread(wait_for_helper, sub_info,CLONE_FS | CLONE_FILES | SIGCHLD);elsepid = kernel_thread(____call_usermodehelper, sub_info,CLONE_VFORK | SIGCHLD);

情况1:需要等待完成(sub_info->wait为真)

pid = kernel_thread(wait_for_helper, sub_info, CLONE_FS | CLONE_FILES | SIGCHLD);
  • 执行函数: wait_for_helper
  • 克隆标志:
    • CLONE_FS: 共享文件系统信息
    • CLONE_FILES: 共享文件描述符表
    • SIGCHLD: 子进程终止时向父进程发送信号
  • 特点: 父线程会等待子进程完全执行完毕

情况2:不需要等待完成(sub_info->wait为假)

pid = kernel_thread(____call_usermodehelper, sub_info, CLONE_VFORK | SIGCHLD);
  • 执行函数: ____call_usermodehelper(实际启动用户程序的函数)
  • 克隆标志:
    • CLONE_VFORK: 关键标志,父线程等待子线程执行execve()
    • SIGCHLD: 子进程终止时发送信号
  • 特点: 父线程只等待到子进程成功执行execve(),不等待用户程序执行完成

第16-21行:处理线程创建结果

        if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);} else if (!sub_info->wait)complete(sub_info->complete);

情况1:线程创建失败(pid < 0)

if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);
}
  • pid < 0: 表示kernel_thread调用失败
  • sub_info->retval = pid: 将错误码保存到返回结果中
  • complete(sub_info->complete): 立即触发完成量,通知等待者
  • 结果: 调用者立即得到错误返回

情况2:线程创建成功且不需要等待

else if (!sub_info->wait)complete(sub_info->complete);
  • 条件: 线程创建成功且sub_info->wait为假
  • complete(sub_info->complete): 立即触发完成量
  • 逻辑: 对于非等待模式,一旦成功创建了执行用户程序的内核线程,就认为任务完成

情况3:线程创建成功且需要等待

  • 隐含逻辑:
  • 不立即触发完成量
  • wait_for_helper函数会在用户程序执行完成后触发完成量

克隆标志的深层含义

CLONE_VFORK的关键作用:

// 没有CLONE_VFORK的情况
父线程创建子线程 → 立即继续执行
子线程准备execve时,父线程可能已经释放了sub_info内存 → 崩溃!// 有CLONE_VFORK的情况  
父线程创建子线程 → 父线程阻塞等待
子线程执行execve成功 → 父线程被唤醒继续执行
子线程执行execve失败 → 父线程也被唤醒

错误处理策略

// 线程创建失败的场景
kernel_thread失败 → pid = 负数错误码
立即设置retval = 错误码
立即触发complete通知调用者
调用者得到错误信息// 线程创建成功的场景
pid = 有效的进程ID
根据wait标志决定何时通知调用者

管理用户空间进程的执行和状态收集wait_for_helper

static int wait_for_helper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;struct k_sigaction sa;/* Install a handler: if SIGCLD isn't handled sys_wait4 won't* populate the status, but will return -ECHILD. */sa.sa.sa_handler = SIG_IGN;sa.sa.sa_flags = 0;siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);allow_signal(SIGCHLD);pid = kernel_thread(____call_usermodehelper, sub_info, SIGCHLD);if (pid < 0) {sub_info->retval = pid;} else {/** Normally it is bogus to call wait4() from in-kernel because* wait4() wants to write the exit code to a userspace address.* But wait_for_helper() always runs as keventd, and put_user()* to a kernel address works OK for kernel threads, due to their* having an mm_segment_t which spans the entire address space.** Thus the __user pointer cast is valid here.*/sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);}complete(sub_info->complete);return 0;
}

函数功能

这个函数负责等待用户模式助手进程执行完成,并收集其退出状态。它在独立的内核线程中运行,专门用于管理用户空间进程的执行和状态收集

代码逐段解释

第1-4行:函数声明和变量定义

static int wait_for_helper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;struct k_sigaction sa;
  • static int: 静态函数,返回整型状态
  • void *data: 泛型指针参数,实际指向struct subprocess_info
  • struct subprocess_info *sub_info = data: 转换为具体的子进程信息结构体
  • pid_t pid: 存储实际执行用户程序的内核线程PID
  • struct k_sigaction sa: 内核信号处理结构,用于配置SIGCHLD信号处理

第6-12行:信号处理配置

        /* Install a handler: if SIGCLD isn't handled sys_wait4 won't* populate the status, but will return -ECHILD. */sa.sa.sa_handler = SIG_IGN;sa.sa.sa_flags = 0;siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);allow_signal(SIGCHLD);

详细信号配置过程:

sa.sa.sa_handler = SIG_IGN;  // 设置信号处理函数为忽略
sa.sa.sa_flags = 0;          // 无特殊标志
siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));  // 设置信号掩码,阻塞SIGCHLD
do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);  // 应用信号处理配置
allow_signal(SIGCHLD);       // 允许当前线程接收SIGCHLD信号

信号处理的重要性

  • 注释说明: 如果不处理SIGCHLD信号,sys_wait4会返回-ECHILD错误而不会填充状态
  • SIG_IGN: 忽略SIGCHLD信号,让wait4正常工作
  • 信号掩码: 确保在执行期间不会意外处理SIGCHLD信号

第14-16行:创建实际的工作线程

        pid = kernel_thread(____call_usermodehelper, sub_info, SIGCHLD);if (pid < 0) {sub_info->retval = pid;
  • kernel_thread: 创建新的内核线程
  • 执行函数: ____call_usermodehelper - 实际启动用户空间程序的函数
  • 克隆标志: SIGCHLD - 子进程终止时发送信号
  • 错误处理: 如果线程创建失败(pid < 0),将错误码保存到retval

第17-28行:等待子进程完成

        } else {/** Normally it is bogus to call wait4() from in-kernel because* wait4() wants to write the exit code to a userspace address.* But wait_for_helper() always runs as keventd, and put_user()* to a kernel address works OK for kernel threads, due to their* having an mm_segment_t which spans the entire address space.** Thus the __user pointer cast is valid here.*/sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);}

关键注释解释:

  • 通常问题: 在内核中调用wait4是有问题的,因为wait4期望将退出代码写入用户空间地址
  • 特殊情况: wait_for_helper总是作为keventd运行,内核线程的mm_segment_t覆盖整个地址空间
  • 结论: 将内核地址强制转换为__user指针在这里是有效的
sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);
  • 参数:
    • pid: 要等待的子进程ID
    • (int __user *) &sub_info->retval: 存储退出状态的位置(强制转换)
    • 0: 无选项
    • NULL: 不返回资源使用信息
  • 作用: 阻塞等待指定PID的进程退出,并将退出状态保存到sub_info->retval

第30-31行:通知完成并返回

        complete(sub_info->complete);return 0;
  • complete(sub_info->complete): 触发完成量,通知等待的调用者进程已结束
  • return 0: 函数成功返回0(wait_for_helper自身的状态,不是用户进程的状态)

完整的执行流程

Callerwait_for_helperExecutorUser Program创建线程执行wait_for_helper配置SIGCHLD信号处理创建线程执行____call_usermodehelper执行用户空间程序程序执行完成/退出线程退出sys_wait4捕获退出状态complete通知完成获取sub_info->>retvalCallerwait_for_helperExecutorUser Program

初始化信号集siginitset

static inline void siginitset(sigset_t *set, unsigned long mask)
{set->sig[0] = mask;switch (_NSIG_WORDS) {default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;case 2: set->sig[1] = 0;case 1: ;}
}

函数功能

这个函数用于初始化信号集(sigset_t),设置第一个信号字的掩码,并将其余信号字清零。它是信号处理的基础函数,确保信号集处于已知的初始状态

代码逐段解释

第1行:函数声明

static inline void siginitset(sigset_t *set, unsigned long mask)
  • static inline: 静态内联函数,建议编译器将代码直接插入调用处
  • void: 无返回值
  • sigset_t *set: 指向信号集结构的指针,信号集用于表示一组信号
  • unsigned long mask: 位掩码,每个位代表一个信号(1-32)

第2行:设置第一个信号字

        set->sig[0] = mask;
  • set->sig[0]: 信号集的第一个"字"(word)
  • mask: 32位(或64位)的位掩码
  • 作用: 设置信号1-32的掩码,每个位对应一个信号号

第3-10行:清零其余信号字

        switch (_NSIG_WORDS) {default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;case 2: set->sig[1] = 0;case 1: ;}

情况1: default (信号字数大于2)

default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;
  • 条件: 当_NSIG_WORDS > 2时执行
  • &set->sig[1]: 从第二个信号字开始
  • sizeof(long)*(_NSIG_WORDS-1): 计算需要清零的字节数
  • memset: 批量清零剩余的所有信号字

情况2: case 2 (正好2个信号字)

case 2: set->sig[1] = 0;
  • 条件: 当_NSIG_WORDS == 2时执行
  • set->sig[1] = 0: 手动清零第二个信号字
  • 注意: 没有break语句,会继续执行到case 1

情况3: case 1 (只有1个信号字)

case 1: ;
  • 条件: 当_NSIG_WORDS == 1时执行
  • 空语句: ;: 什么都不做,因为只有一个信号字,已经在第2行设置过了

允许当前线程接收指定的信号allow_signal

int allow_signal(int sig)
{if (sig < 1 || sig > _NSIG)return -EINVAL;spin_lock_irq(&current->sighand->siglock);sigdelset(&current->blocked, sig);if (!current->mm) {/* Kernel threads handle their own signals.Let the signal code know it'll be handled, sothat they don't get converted to SIGKILL orjust silently dropped */current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;}recalc_sigpending();spin_unlock_irq(&current->sighand->siglock);return 0;
}

函数功能

allow_signal函数用于允许当前线程接收指定的信号。它主要在内核线程中使用,因为内核线程默认阻塞所有信号,需要显式允许才能接收特定信号

代码逐段解释

第1-2行:函数声明和参数验证

int allow_signal(int sig)
{if (sig < 1 || sig > _NSIG)return -EINVAL;
  • 参数: sig - 要允许的信号编号
  • 验证: 检查信号编号是否在有效范围内(1到_NSIG)
  • 错误处理: 如果信号编号无效,返回-EINVAL(无效参数错误)

第4行:获取信号处理锁

        spin_lock_irq(&current->sighand->siglock);
  • spin_lock_irq: 获取自旋锁并禁用中断
  • current->sighand->siglock: 当前进程信号处理器的自旋锁
  • 目的: 防止在修改信号状态时发生竞争条件

第5行:从阻塞掩码中移除信号

        sigdelset(&current->blocked, sig);
  • current->blocked: 当前线程的阻塞信号集
  • sigdelset: 从阻塞集中删除指定信号
  • 效果: 该信号现在可以被传递给当前线程

第6-13行:内核线程的特殊处理

        if (!current->mm) {/* Kernel threads handle their own signals.Let the signal code know it'll be handled, sothat they don't get converted to SIGKILL orjust silently dropped */current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;}
  • !current->mm: 检查当前线程是否是内核线程

    • 用户空间进程有内存描述符(mm)
    • 内核线程的mm为NULL
  • 内核线程自己处理信号:与用户进程不同,内核线程需要显式处理信号

  • 目的:让信号处理代码知道这个信号会被处理,避免信号被转换为SIGKILL或静默丢弃

current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;
  • (void __user *)2: 魔数指针值,不是真正的函数指针
  • 含义:告诉内核信号处理系统,这个信号由内核线程自己处理
  • 索引action[(sig)-1] - 信号动作数组从0开始索引

第14行:重新计算挂起信号状态

        recalc_sigpending();
  • 功能:重新计算当前线程的挂起信号状态
  • 必要性:由于阻塞掩码改变,可能需要更新TIF_SIGPENDING标志
  • 作用:确保信号传递机制能够正确工作

第15-16行:释放锁并返回

        spin_unlock_irq(&current->sighand->siglock);return 0;
  • spin_unlock_irq: 释放自旋锁并恢复中断状态
  • return 0: 返回成功状态

设计原理

为什么内核线程需要特殊处理?

  1. 无用户上下文: 内核线程没有用户空间,无法执行用户注册的信号处理函数
  2. 默认阻塞: 内核线程默认阻塞所有信号,避免意外中断内核操作
  3. 显式控制: 要求内核线程显式声明它们要处理哪些信号

总结

allow_signal函数是Linux内核信号处理机制的关键组成部分,特别是对于内核线程的信号管理:

  1. 主要用途: 允许内核线程接收特定信号
  2. 核心操作: 从阻塞掩码中移除信号
  3. 特殊处理: 为内核线程设置特殊的信号处理标记
  4. 线程安全: 使用自旋锁保护信号状态修改
  5. 状态同步: 确保信号挂起状态正确更新

调用execve启动用户空间程序____call_usermodehelper

static int ____call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;int retval;/* Unblock all signals. */flush_signals(current);spin_lock_irq(&current->sighand->siglock);flush_signal_handlers(current, 1);sigemptyset(&current->blocked);recalc_sigpending();spin_unlock_irq(&current->sighand->siglock);/* We can run anywhere, unlike our parent keventd(). */set_cpus_allowed(current, CPU_MASK_ALL);retval = -EPERM;if (current->fs->root)retval = execve(sub_info->path, sub_info->argv,sub_info->envp);/* Exec failed? */sub_info->retval = retval;do_exit(0);
}

函数功能

这个函数是用户模式助手机制的核心执行函数,负责在内核线程的上下文中执行用户空间程序。它是实际调用execve来启动用户空间程序的地方

代码逐段解释

第1-3行:函数声明和变量定义

static int ____call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;int retval;
  • static int: 静态函数,返回整型状态
  • void *data: 泛型指针参数,实际指向struct subprocess_info
  • struct subprocess_info *sub_info = data: 转换为子进程信息结构体指针
  • retval: 存储执行结果的变量

第5-6行:刷新信号处理

        /* Unblock all signals. */flush_signals(current);
  • 解除所有信号的阻塞
  • flush_signals(current): 刷新当前线程的信号状态
    • 清除挂起的信号
    • 重置信号处理状态
    • 确保执行用户程序时信号处理处于干净状态

第7-12行:信号处理环境清理

        spin_lock_irq(&current->sighand->siglock);flush_signal_handlers(current, 1);sigemptyset(&current->blocked);recalc_sigpending();spin_unlock_irq(&current->sighand->siglock);
  1. spin_lock_irq(&current->sighand->siglock): 获取信号处理锁并禁用中断
  2. flush_signal_handlers(current, 1):
    • 刷新信号处理程序
    • 第二个参数1表示重置为默认处理(SIG_DFL)
  3. sigemptyset(&current->blocked):
    • 清空阻塞信号集
    • 确保没有信号被阻塞,用户程序可以接收所有信号
  4. recalc_sigpending(): 重新计算挂起信号状态
  5. spin_unlock_irq(&current->sighand->siglock): 释放锁并恢复中断

第14-15行:设置CPU亲和性

        /* We can run anywhere, unlike our parent keventd(). */set_cpus_allowed(current, CPU_MASK_ALL);
  • 可以在任何CPU上运行,与父进程keventd不同
  • set_cpus_allowed(current, CPU_MASK_ALL):
    • 设置当前线程可以在所有CPU上运行
    • CPU_MASK_ALL: 所有CPU的位掩码
    • 提供更好的负载均衡和性能

第17-19行:执行用户空间程序

        retval = -EPERM;if (current->fs->root)retval = execve(sub_info->path, sub_info->argv, sub_info->envp);
  1. retval = -EPERM: 默认返回值设为"操作不允许"错误
  2. if (current->fs->root): 检查当前线程是否有有效的根文件系统
    • 这是执行execve的前提条件
    • 内核线程必须有有效的文件系统上下文才能执行用户程序
  3. execve(sub_info->path, sub_info->argv, sub_info->envp):
    • 执行用户空间程序
    • sub_info->path: 程序路径
    • sub_info->argv: 参数数组
    • sub_info->envp: 环境变量数组

execve的特殊性:

  • 如果成功,这个函数不会返回(进程被用户程序替换)
  • 如果失败,返回错误码

第21-23行:处理执行失败

        /* Exec failed? */sub_info->retval = retval;do_exit(0);
  • sub_info->retval = retval: 将执行结果保存到子进程信息结构中
    • 如果execve成功,这行代码不会执行
    • 只有execve失败时才会执行到这里
  • do_exit(0): 终止当前内核线程
    • 参数0是退出码
    • 线程资源被回收

关键设计原理

信号环境清理的重要性

// 为什么需要彻底清理信号环境?
// 1. 内核线程可能设置了特殊的信号处理
// 2. 用户程序应该从干净的信号状态开始
// 3. 避免内核信号处理影响用户程序行为// 具体清理内容:
flush_signals:     清除挂起信号
flush_signal_handlers: 重置处理程序为默认
sigemptyset:       清空阻塞信号集

执行上下文准备

// 内核线程执行用户程序的挑战:
// 1. 文件系统上下文: 需要有效的root
// 2. 信号处理: 需要重置为用户预期状态  
// 3. CPU调度: 允许在所有CPU上运行提高性能
// 4. 内存管理: execve会处理地址空间切换

错误处理策略

// 执行失败时的处理:
// 1. 保存错误码: 让调用者知道失败原因
// 2. 线程退出: 清理资源,避免僵尸内核线程
// 3. 通过完成量机制通知等待者
http://www.dtcms.com/a/508021.html

相关文章:

  • 建网站空间的详细说明网站备案怎么查询
  • 2025 兽用 mRNA 疫苗市场调研:58.7% CAGR 下,技术路线与投资前景深度分析
  • 关于 Qt5.x版本离线安装可以跳过登录但是实际离线仍需要登录 的解决方法
  • 什么时候会出现电源平面谐振?
  • php做网站常见实例新市网站建设
  • 【Vue知识点总结】style标签的 scoped 属性
  • 网站移动适配怎么做济南做网站互联网公司排名
  • authui!CLogonFrame::Create中的USER32!LoadImageW可以作为有效起始断点
  • Linux服务器编程实践50-TCP接收与发送缓冲区:SO_RCVBUF与SO_SNDBUF设置
  • 免费无版权图片素材网站中国制造网简介
  • 鸿蒙Next Test Kit:一站式自动化测试框架详解
  • 《微信小程序》第一章:开发前准备与配置
  • 实验二-决策树-葡萄酒
  • 用双语网站做seo会不会建设一个网站需要哪些员工
  • 专项智能练习(教学过程的规律)
  • 设计模式-创建型设计模式
  • 非关系型数据库(NoSQL)学习指南:从入门到实战
  • Endnote | word中参考文献段落对齐及悬挂缩进的设置
  • MCU硬件学习
  • SpringBoot教程(十九) | SpringBoot集成Slf4j日志门面(优化版)
  • 帮别人备案网站大连企业网站建设模板
  • 关于反向传播
  • --- 数据结构 AVL树 ---
  • 8、docker容器跨主机连接
  • 怎么建网站教程视频app网站开发软件、
  • Python 检测运动模糊 源代码
  • PHP面试题——字符串操作
  • SOLIDWORKS 2025——2D与3D的集成得到了显著提升
  • TypeScript函数与对象的类型增强
  • 专业做网站方案手机登录不了建设银行网站