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

Linux操作系统学习

学习内容来自于尚硅谷B站课程:https://www.bilibili.com/video/BV1DJ4m1M77z/?spm_id_from=333.337.search-card.all.click

Linux内核理解

内核是操作系统的核心部分,负责管理计算机的硬件资源,包括处理器、内存、存储设备和其他外围设备。内核提供系统服务的基础,如进程管理、内存管理、设备驱动、文件系统和网络通信等内核作为硬件和应用程序之间的中介,提供一个抽象层,使得应用程序不需要直接与硬件交互。

应用程序

应用程序是运行在操作系统之上的软件,用于执行特定的任务,如文本编辑、图像处理、网络通信等。

进程和程序

一个程序(我们俗称main())里面可能会写多个进程。按照程序执行到的顺序去启动对应进程。

内核会保存每个进程的一些信息,保存地点称为进程控制块(Process Control Block,PCB),方便管理进程

保存的内容如下

  • 进程编号(PID),每个进程对应的唯一编号,一般为正整数形式。
  • 进程状态信息。

进程的切换(进程状态)是由操作系统的CPU调度器完成的

进程的抽象状态有

  • 初始态(Initial)

这是进程生命周期的开始阶段,进程被创建时处于初始态,或者叫创建态。在这个阶段,操作系统为新进程分配资源。

  • 就绪态(Ready)

就绪态意味着进程已准备好运行,但由于CPU调度算法或其他正在运行的进程,它当前没有运行。

  • 运行态(Running)

当进程正在CPU上执行时,它处于运行态。

  • 阻塞态(Blocked)

进程由于等待某个事件(如IO操作完成)而无法继续执行时,它就处于阻塞态。

  • 终止态(Final)

进程执行完毕,并释放其占用的所有资源,进行必要的清理工作。此时,虽然进程已结束了所有活动,但操作系统内核仍保留它的PCB,进程处于终止态。通常,终止态持续时间非常短暂,PCB很快会被内核释放。

  • 僵尸态(Zombie)

僵尸态与终止态非常相似,区别是,如果进程结束了所有工作后PCB长期未被释放,它就处于僵尸态。在进程状态机中,我们对终止态和僵尸态不作区分,因为二者都是进程任务执行完毕之后的状态,意味着进程生命周期的终止。

Linux进程状态实现

  • D: 不可中断睡眠状态(通常是在进行IO操作)
  • I: 空闲的内核线程
  • R: 运行或可运行状态(在运行队列中)
  • S: 可中断睡眠状态(等待事件完成)
  • T: 由工作控制信号停止
  • t: 在跟踪过程中由调试器停止
  • W: 分页(从2.6.xx内核版本开始就不再有效)
  • X: 死亡状态(永远不应该被看到),PCB已被清理
  • Z: 僵尸进程,已经终止但尚未被父进程回收

抽象理论

Linux实现

初始态

这个阶段通常很短暂,不对应于Linux的特定进程状态。

就绪态

对应于R

运行态

对应于R

阻塞态

D、S、T、t均属于阻塞态

僵尸态

Z

内核线程执行内核级任务,不属于用户线程。

X表示PCB已被清理,不应被看到。

  • 进程切换时(运行一个时间帧会产生中断,即切换,需要保存当前运行信息)需要保存和恢复的一些CPU寄存器,其中关键的有程序计数器(Program Counter)的值,用于记录进程恢复时应执行的指令地址。
  • 内存管理信息,如页表、内存限制、段表等。
  • 当前工作目录(Current Working Directory)。
  • 进程调度信息,包括进程优先级、调度队列指针等。
  • I/O状态信息,包括分配给进程的I/O设备列表,打开的文件描述符表等,后者包含很多指向file结构体的指针。
  • 同步和通信信息,包括信号量、信号、等用于进程同步和通信机制的信息。
  • 用户id和组id。

在Linux内核中,进程控制块(PCB)的实现是struct task_struct,上述信息都存储在这个结构体中。

进程的内存模型

所有进程只有Kernel Space是共有的,下面其他内存空间是各个进程独有的

内核空间是进程虚拟内存中保留给操作系统内核的部分,用于存放内核代码和数据(简单的说就是 fork(创建进程)、exec(执行程序)等系统调用的底层实现代码等)。这部分空间对用户程序是不可见、不可直接访问的。内核空间具有最高的访问权限,只有内核态下的代码可以执行这里的操作。所有进程的内核空间是共享的。

其他空间称为用户空间。用户空间是内存中分配给用户程序的部分,与内核空间相隔离。用户程序和库函数在这里执行。用户空间的代码运行在用户态,拥有较低的权限,不能直接执行特权操作或访问内核空间。

虚拟内存和物理内存

虚拟内存

虚拟内存是计算机系统内存管理的一种技术,它为每个进程提供了一种“虚拟”的地址空间,这个地址空间对于每个程序来说看起来都是连续的,但实际上可能被分散地存储在物理内存和磁盘上(如交换空间或页面文件)。虚拟内存允许系统超额分配内存,即分配的内存总量可以超过物理内存的实际容量。虚拟内存简化了内存的管理,使得应用程序不需要关心物理内存的实际情况。​​​

物理内存

物理内存指的是计算机中安装的实际RAM(随机访问存储器)模块。它是系统用来存储正在运行的程序和数据的硬件资源。

物理内存直接影响到计算机能够同时处理的信息量。更多的物理内存意味着可以同时运行更多的程序,或者处理更大的数据集。

MMU

进程可以直接操作的只有虚拟内存,那么,虚拟内存毕竟是“虚拟”的,进程的代码段、数据、栈等最终一定要存储到真正的物理内存,那么,我们就要建立虚拟内存和物理内存之间的映射关系。在操作系统中,这件事是由MMU来完成的。

MMU(Memory Management Unit)是CPU的一个组成部分,负责处理虚拟地址到物理地址的转换。当程序试图访问一个虚拟内存地址时,MMU会查询页表来找到对应的物理内存地址,然后完成内存访问。MMU还负责检查访问权限,确保程序不会访问未授权的内存区域。

此外,当所请求的虚拟地址没有映射到物理地址或虚拟页对应的数据位于磁盘的交换空间中时,即产生缺页故障,MMU会通知操作系统,由操作系统来处理这种情况,为虚拟页分配页帧,或将数据从硬盘加载到内存的一个页帧中,然后更新页表以建立新的映射关系。完成这些操作后,当程序再次尝试访问原来的虚拟地址时,可以正确地访问到映射后的物理页帧中的数据 。

MMU本身是硬件组件,操作系统负责配置和管理MMU使用的数据结构(如页表),以及处理MMU生成的各种内存管理相关的异常(如缺页故障)。这种协作机制允许操作系统利用MMU提供的硬件支持,实现虚拟内存、内存保护和其他高级内存管理功能。

​​​​​​​

在操作系统的上下文中,“页”(Page)是虚拟内存管理中的一个基本单位,通常大小为4KB或2MB等,具体大小依赖于处理器和操作系统的设计。操作系统使用页来实现虚拟内存。

​​​​​​​页帧

页帧(Page Frame),也称为物理页(Physical Page),是物理内存中的一个固定大小的区块。在虚拟内存系统中,物理内存被划分为许多这样大小相等的页帧,以便于内存的管理和映射。

​​​​​​​页表

页表是操作系统用于管理虚拟内存系统中的虚拟地址到物理地址映射的数据结构。页表包含页表项(Page Table Entries, PTEs),每个PTE对应一个页,包含该页映射到的页帧的物理地址及访问该页的权限和状态(如是否在物理内存中,是否可写等)。在x86-64架构下,页表大小为4K。

​​​​​​​TLB

TLB全称为Translation-Lookaside Buffer,即地址转换旁路缓存。是MMU中一个组成部分,是硬件为提升虚拟内存到物理内存地址映射的效率而提供的硬件支持。

​​​​​​​多级页表

在32位系统中,每个进程的虚拟内存地址空间为4GB,假设页大小为4KB,则一个进程的虚拟地址空间要分为1M页,假定每个PTE的大小为4字节,每个进程就需要占用4MB的空间来存储页表,启动100个进程,仅页表就要占用400MB的内存空间。而很少有进程会用到所有的虚拟页,这样无疑会带来巨大的内存浪费。

为了解决这个问题,引入了多级页表。此处不做展开。多级页表中,除最后一级外,都可以叫做页目录(Page Directory)。

虚拟内存如何映射到物理内存

按图中所示只有VPN0、3、5、7存储了数据,当需要用到这个数据时,首先会报缺页故障,通过MMU来将虚拟内存映射到物理内存上。(数据映射不是一个字节一个字节的映射,而是一页一页的去映射)

进程如何共享存储

​​​​​​​通过shm_open创建内存共享对象,再通过mmap()将进程的虚拟内存映射到共享对象。同理其他进程也是这样。

如下程序

int main() {int val = 123;// 定义变量接收子进程PID__pid_t pid;// 创建一个子进程if ((pid = fork()) > 0 ){sleep(1);printf("父进程中val 的内容是: %d\nval 所在的地址是: %p\n", val, &val);} else if (pid == 0) {val = 321;printf("子进程中val 的内容是: %d\nval 所在的地址是: %p\n", val, &val);} else {printf("子进程创建失败\n");}return 0;
}

得到结果是:val的值不同,但地址是一样的

原因是:&val 是虚拟地址(进程 “认为” 的地址),而非真实的物理内存地址。父子进程的虚拟地址可以相同,但背后映射的物理内存在子进程修改数据后会分离,因此值互不影响

可以这么理解:

  • 父进程有一本笔记本(物理内存),上面写着 val=123,笔记本的 “标签” 是虚拟地址 0x1234(父进程的门牌号)。

  • fork() 后,子进程也拿到了一个 “标签” 相同的 0x1234,但最初指向的是同一本笔记本(共享物理内存)。

  • 当子进程要改 val 时,操作系统会触发写时复制(Copy-On-Write) 机制(即系统会给子进程复制一本新笔记本(新物理内存)),子进程的 0x1234 标签改指向这本新笔记本,子进程在新笔记本上写 321

  • 父进程的 0x1234 标签仍指向原来的笔记本,所以看到的还是 123

CPU运行状态

​​​​​​​内核态

内核态是CPU的一种运行模式,具有执行所有指令和访问所有硬件资源的权限。在这种模式下,操作系统内核执行其核心功能。所有与硬件交互的操作都必须在内核态下执行。

由于具有完全的系统控制权,任何在内核态执行的代码都必须是高度可靠的,以避免系统崩溃或安全漏洞。

​​​​​​​用户态

用户态是CPU的另一种运行模式,权限受限。应用程序在用户态下运行,不能直接执行特权指令或访问受保护的内存区域。

用户态为应用程序提供了一个安全的执行环境,通过系统调用请求操作系统提供的服务。

​​​​​​​特权指令

特权指令是指只有在内核态下才可以执行的指令。这些指令提供了对硬件和关键系统资源的直接控制能力,因此它们的执行被严格限制在操作系统内核中,以防止恶意软件或错误的程序代码破坏系统的稳定性和安全性。

只有在内核态下才可以执行特权指令。

中断和异常

大类

子类

原因

异步/同步

返回行为

中断(Interrupt

中断

来自硬件的信号

异步

总是返回到下一条指令

异常(Exception

陷阱

进程主动触发的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

​​​​​​​中断

中断(Interrupt)通常是I/O设备或时钟触发的,信号来自处理器外部,不是由任何一条指令造成的,从这个角度讲,它是异步的。中断处理完毕后总是执行下一条指令。

​​​​​​​异常

异常是(Exception)CPU执行指令时检测到特定条件触发的。x86-64架构定义了三种异常:陷入(Trap)、故障(Fault)和终止(Abort)。

① 陷入

是由进程执行陷入指令(可以切换到内核态的指令)主动触发的,是同步的,执行完毕后总是执行下一条指令。

② 故障

故障由错误情况引起,可能被故障处理程序修正,如上文提到的缺页故障就是故障的一种。故障发生时,处理器将控制权交由故障处理程序,如果故障被修复,则返回引起故障的指令,并重新执行。否则,处理程序返回到内核中的abort例程,后者终止引起故障的进程。

③ 终止

终止是不可恢复的致命错误造成的结果。如底层硬件错误,或者进程产生的算数异常,无法被修复。终止发生时,CPU将控制权交由终止处理程序,这个程序不会将CPU的控制权返还给应用程序,而是返回到内核中的abort例程,后者终止进程。

进程创建过程

fork()创建进程

主要完成了以下工作:

  • 为子进程创建内核栈、thread_info实例。
  • 复制父进程的task_struct,后者包含了内核栈、虚拟内存管理信息、打开的文件描述符表等的指针,此时子进程只是复制了这些资源的引用。
  • 清除子进程的统计信息,更新子进程task_struct的标志位。
  • 为子进程分配新的PID,将子进程的PPID设置为调用fork()的进程。
  • 清除与fork()返回值相关的寄存器,使得子进程中fork()返回的是0。
  • 复制打开的文件描述符表,这一过程底层被指向的struct file实例中引用计数加一。复制文件系统信息、复制地址空间(页表相关信息),复制信号处理信息。
  • 最后,如果子进程成功创建则被唤醒,处于就绪态。

除了进程号不同,子进程原封不动的复制父进程所有的内容

​​​​​​​COW

写时复制机制(Copy on Write COW)可以提高进程创建效率。子进程完整地复制了父进程的地址空间,此时父子进程的虚拟内存空间映射到相同的物理内存空间。只有当二者之一执行了写入操作才会复制写入区域的内容,为父子进程维护不同的物理页帧。

当一个进程,如子进程向某一页写入数据,,子进程的改虚拟页会映射到其他物理页上

​​​​​​​execve()调用过程

​​​​​​​参数和环境准备

内核检查传递给execve()的参数,包括可执行文件的路径、环境变量和命令行参数,以确保它们的有效性和安全性。这个阶段内核会在内核空间中准备一份新程序需要的命令行参数和环境变量的备份。

​​​​​​​打开和验证可执行文件

打开指定的二进制文件,验证其格式是否支持(例如,ELF格式),并检查执行权限。如果这一步找不到可执行文件的路径,就会直接终止。

​​​​​​​创建新的内存映射

清除进程当前的内存映射,包括用户空间中的代码、数据、堆和栈。

根据新的程序建立新的代码段、数据段、堆和栈等。

需要注意的是,内存映射不包含内核空间,内核空间的映射是由操作系统内核管理的,对所有进程是共享的。execve切换的只是用户空间

​​​​​​​复制参数和环境变量

在新的地址空间中为命令行参数和环境变量分配空间,并将内核中它们的备份复制到新的位置。

​​​​​​​初始化进程上下文

设置新的程序计数器、栈指针等,以便新程序可以正确执行。

清理和重设进程的各种内核资源,如文件描述符表。根据文件描述符的 close-on-exec 标志(FD_CLOEXEC)进行处理,如果有该标志,则文件描述符被关闭。

​​​​​​​更新 task_struct 和其他内核结构

更新 task_struct 中关于进程地址空间、堆栈、命令行参数、环境变量的指针。

重置信号控制信息到默认状态。

清理进程的各种内核状态,如未处理的信号、定时器等。

​​​​​​​执行新程序

跳转到新加载程序的入口点开始执行。

​​​​​​​进程组

两个进程同属于一个进程组。通过fork()创建的子进程属于父进程创建的进程组,父进程是该组的组长。

进程切换过程

​​​​​​​进程切换的场景

如果进程的运行不会被打断,那么操作系统内核想要回CPU的控制权就只能寄希望于进程主动归还,或者强制重启计算机。现代计算机提供了中断和异常机制,二者都可以打断正在执行的进程,将CPU的控制权交还给内核。进程的切换需要内核介入,必然要通过中断或异常来实现。进程切换主要在以下几种情况下发生。

  • 时钟中断触发,被中断的进程获得的CPU时间片耗尽,操作系统决定切换进程。
  • 当前进程发生故障,内核夺回CPU控制权,如果故障无法被修复,则内核终止该进程,切换至其它进程。
  • 时钟中断触发,当前进程在等待IO操作,为避免资源浪费,切换至其他进程。
  • 时钟中断触发,高优先级进程处于就绪状态,内核将CPU使用权由当前进程转交给高优先级进程。

进程切换过程

进程的切换需要借助中断或异常,流程如下。假设正在运行的进程A要被切换到进程B。

  • CPU暂存栈指针、程序计数器、段选择器和状态寄存器的值。
  • 栈指针由进程A的用户栈切换至它的内核栈,操作系统切换至内核态。
  • CPU将第一步暂存的寄存器值压入内核栈。
  • 将错误码压入内核栈。
  • 程序计数器指向中断或异常处理程序。
  • 操作系统执行中断或异常处理程序。
  • 在中断或异常处理程序中,调度器会判断是否满足进程切换条件,如果满足则执行以下操作:

①将进程A所有相关寄存器的值保存至进程A的PCB(Linux底层实现为struct task_struct)。这会包含它的页表基址。

②有些架构会清除TLB。

③将进程B的PCB中记录的页表基址、栈指针等寄存器信息加载(恢复)到对应寄存器。此时栈指针指向进程B的内核栈。进程A回到调度队列。如果进程A是因为CPU时间片耗尽,则处于就绪状态,回到就绪队列。

要注意,打开的文件描述符表等相关资源的切换不需要通过寄存器实现,这些资源存储在struct task_struct结构体中,调度器可以从调度队列获得task_struct,完成资源切换。

  • 执行权限检查,判断当前是否处于内核态。
  • 从进程B的内核栈恢复CS和程序计数器,后者指向B的用户进程代码。
  • 恢复进程B的状态寄存器RFLAGS。
  • 从进程B的内核栈恢复SS和栈指针,后者指向进程B的用户栈,此时切换到用户态。
  • 在用户态下继续进程B的执行,进程切换完成。

​​​​​​​系统调用和库函数

​​​​​​​系统调用

系统调用是操作系统提供的服务接口,允许用户空间的应用程序请求操作系统执行特定的功能,这些功能通常涉及到资源管理、文件操作、进程控制等更深层次的系统任务。系统调用是操作系统内核功能的直接接口。

用户在程序中执行系统调用,CPU会执行陷入指令,切换到内核态,执行相应的异常处理程序,实现用户进程对于内核功能的调用。

系统调用运行在内核态,具有访问硬件和管理系统资源的权限。涉及用户态到内核态的切换,执行开销较大。

​​​​​​​库函数

库函数通常是用高级语言编写,库函数存储在函数库中,最常见的是C标准函数库。

在使用标准输入输出的时候,我们都需要引入一个头文件:stdio.h,这个头文件是C标准库的IO接口的声明。

C标准函数库(C Standard Library,简称libc)是一个由ANSI C标准定义的函数库集合,它为C语言程序员提供了一组标准化的程序接口,这些接口实现了基础的程序功能,如输入输出处理、字符串操作、数学计算等。这个库是C语言标准的一部分,所有遵循标准的C语言实现都必须提供这个库的功能。

在现代Linux发行版中,使用最多的libc实现为GNU C函数库,简称glibc,它是C标准库的一个实现,它是自由软件基金会(Free Software Foundation, FSF)GNU项目的一部分。glibc为C语言提供了标准库所要求的所有功能,并且还包括了一些扩展功能和优化,以支持GNU/Linux系统和其他类UNIX系统

线程

概念

线程是进程中的执行单元,它共享进程的资源和地址空间,但拥有自己的执行堆栈、程序计数器和一组寄存器。由于线程共享相同进程内的资源,它们之间的通信和数据共享相对容易。在很多类Unix系统中,线程被称为轻量级的进程,创建和上下文切换的开销小于进程。

​​​​​​​线程和进程的区别与联系

在Linux中,线程等同于轻量级进程,二者都有独立的task_struct结构体实例。线程创建和进程创建在技术上是完全等同的,fork()和进程创建函数pthread_create()底层都调用了系统调用clone()。

​​​​​​​创建进程

当我们调用fork()时,等同于调用clone(SIGCHLD, 0),SIGCHLD标志的作用是告诉操作系统:当子进程终止时,父进程应当接收到SIGCHLD信号。这个信号是默认的方式,用于通知父进程其子进程已经结束。这样一来,父进程就可以在子进程退出后执行清理操作。

​​​​​​​创建线程

创建线程时,底层会调用clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0),这些Flag的含义如下。

  • CLONE_VM:共享地址空间。从技术上将,被创建的线程和创建者的struct task_struct实例中struct mm_struct指针类型的字段mm和active_mm指向相同的实例。
  • CLONE_FS:共享文件系统信息。从技术上将,被创建的线程和创建者的struct task_struct实例中struct fs_struct指针类型的字段fs指向相同的实例。
  • CLONE_FILES:共享打开的文件描述符表,从技术上讲,被创建的线程和创建者的struct task_struct实例中struct files_struct指针类型的字段files指向相同的实例。
  • CLONE_SIGHAND:共享信号处理函数表。从技术上讲,被创建的线程和创建者的struct task_struct实例中struct signal_struct指针类型的字段和struct sighand_struct指针类型的字段指向相同的实例。

进程创建和线程创建的区别

从clone()系统调用的角度,我们可以得出结论:如果多个进程共享了地址空间、文件系统信息、打开的文件信息、信号处理信息,那么他们就是同属于一个进程的线程。

task_struct结构体中的pid实际上表示的是进程或线程ID,tgid字段表示的是线程组ID,等同于传统意义上的线程ID。对于单线程的进程,pid字段和tgid字段相同。对于多线程进程,主线程的pid等于tgid,pid此时可以理解为主线程的线程ID或者进程ID,普通线程的tgid等于主线程的pid和tgid,即当前线程所属的线程组ID和进程ID,而pid字段此时相当于线程ID。

线程的特点

​​​​​​​资源共享

线程之间共享进程资源,包括地址空间、文件系统信息、打开的文件描述符和信号处理函数,而不同进程之间的资源是隔离的。

​​​​​​​通信

线程间的通信通常比进程间的通信(例如,通过管道、共享内存)更为高效。因为地址空间是共享的,线程间可以直接通过如全局变量这样的方式通信。

​​​​​​​创建和管理开销

线程的创建和上下文切换通常比进程更轻量级,因此在需要频繁创建和销毁执行单元的场景中,线程可能是更合适的选择。

​​​​​​​线程组

在操作系统中,线程组(Thread Group)是一种将多个线程组织在一起的机制,使得它们可以作为一个单元进行管理和操作。Linux中,同一进程的所有线程同属于一个线程组。线程组ID(Thread Group ID,TGID)用于标识一个线程组,它等于进程号,等于主线程(该进程的第一个线程,创建其它线程的线程)的线程ID(TID)。

​​​​​​​内核线程

上文我们不止一次提到了内核线程。Linux的内核线程是在内核空间中运行的轻量级进程,它们没有独立的地址空间和大部分用户空间资源。内核线程是操作系统内核功能的一部分,主要用于管理和执行内核级任务,如硬件中断处理、系统调用服务、内存管理等。

​​​​​​​内核栈

内核线程拥有自己的内核栈。

​​​​​​​控制内核线程的数据结构

内核线程的信息也存储在task_struct结构体中。

​​​​​​​地址空间

不同于普通进程,struct mm_struct类型的字段mm及active_mm取值为NULL,它们通常共享内核的全局地址空间。

​​​​​​​标记字段

内核线程的task_struct中flags字段被标记为PF_KTHREAD,用于表示这是一个内核线程。

​​​​​​​内核线程的ID

内核线程工作在内核态,相互之间地位是等同的,因此,任意内核线程的TID、TGID、PID都是相同的。并且,所有内核线程的PGID都是0。这是因为内核线程不与任何特定的终端相关联,也不参与普通的作业控制和信号处理,这些通常是用户空间进程的特性。PGID设置为0是设计上的选择,用于确保内核线程在操作系统中的特殊性和隔离性。

http://www.dtcms.com/a/600901.html

相关文章:

  • idea创建javaweb项目
  • 【计网】基于OSPF 协议的局域网组建
  • 开发一个小程序花多少钱
  • Ansible入门详解
  • 一体化系统(一)智慧物业管理综合管理——东方仙盟
  • 买虚机送网站建设wordpress google ad
  • 2008 iis配置网站公司做网站需要注意些什么问题
  • vs2013编译C语言 | 探讨如何使用Visual Studio 2013进行C语言编译与调试
  • k8s上分离集群seatunnel部署(生产推荐)
  • 最新版idea2025 配置docker 打包spring-boot项目到生产服务器全流程,含期间遇到的坑
  • Python 处理 CSV 和 Excel 文件的全面指南
  • 小程序 scroll-view 触底事件不触发问题
  • word内输入带框打对号的数据
  • C语言编译器软件 | 深入了解编译过程与优化技巧
  • Spring框架 - 声明式事务管理
  • html淘宝店铺网站模板辽宁移动网站
  • 微硕WST3404高性能MOSFET,革新汽车雨刮控制系统
  • LeetCode(python)——53.最大子数组的和
  • 其中包含了三种排序算法的注释版本(冒泡排序、选择排序、插入排序),但当前只实现了数组的输入和输出功能。
  • macOS安装SDKMAN
  • LeetCode热题100--78. 子集
  • 攻击链重构的技术框架
  • 商务网站的特点做外贸的人经常逛的网站
  • 网站绑定两个域名怎么做跳转贵阳网络推广公司哪家强
  • 关于sqlite
  • 【C语言】深入理解指针(三)
  • BHYRA:当金融的信任,开始由收益来证明
  • 安装paddle_ocr踩坑(使用PP-OCRv5_server_rec)
  • ClickHouse查看数据库、表、列等元数据信息
  • 场外衍生品系统架构解密:TRS收益互换与场外个股期权的技术实现与业务创新