Linux-线程概念

**摘要:**本文围绕 Linux 线程概念展开,先给出"进程 = 内核数据结构 + 代码和数据""线程是进程内部更轻量化的执行流"的基本结论,再通过重谈虚拟地址空间、页表与物理内存的映射关系,讲清页框、struct page、二级页表与虚拟地址 10/10/12 划分等底层细节。随后结合 pthread_create 代码示例演示多线程的创建与编译链接方式,并从资源划分角度说明线程如何通过不同虚拟地址区域获得不同资源。文章进一步剖析 Linux 内核用进程模拟线程、以轻量级进程(LWP)作为调度单位的实现方式,最后总结线程的优缺点、异常影响与实际用途,为后续线程控制内容做铺垫。

目录

1.Linux线程概念

1.1什么是线程

1.2重谈虚拟地址空间

1.2.1物理内存和磁盘

1.2.2页表

1.2.3一些细节

1.3多线程的实现

1.4理解资源"划分"

1.5Linux内核多线程的实现

1.6理解新的进程和线程概念

1.7线程的优缺点

1.8线程异常

1.9线程用途


1.Linux线程概念

1.1什么是线程

进程是一个运行起来的执行流,一个加载到内存中的程序(教材),我们后面下了一个结论:进程 = 内核数据结构 + 自己的代码和数据。

线程:进程内部的一个执行流,它更轻量化。

观点:进程是系统分配的基本单位(内核),线程是CPU调度的基本单位。

为了理解线程,我们需要重谈虚拟地址空间,之前我们都没怎么讲解页表,这里就可以开始讲解了!

1.2重谈虚拟地址空间

我们之前说,页表是类似C++里的pair键值对,虚拟地址空间的key对应物理地址空间的value,存放在页表中,我们把每一个"键值对"叫做 ---- 页表项。

我们现在基本上已经了解虚拟地址空间的其他东西了,页表到底是什么?

①页表构建映射关系,是按字节映射的吗?(问题)

②页表也是一种内核数据结构,它也要在内核中存在,也要占据物理内存。(事实)

①如果按照字节映射的话,那么虚拟地址空间有4G,那么就是4*1024*1024*1024个字节,因此就对应4GB字节,就有4GB地址,那这样算的话页表就需要存放两倍这么大的空间来表示,因此,页表就需要:一个页表项8个字节,有4GB个地址进行映射,因此就需要8*4*1024*1024*1024个字节,因此需要存放一个页表就需要32GB,这还是一个进程,操作系统有那么多进程,因此不可能有这么多空间来存放,因此页表所谓的映射关系并不是按照字节来映射的!

1.2.1物理内存和磁盘

②我们知道,物理内存在和磁盘进行I/O操作时,大部分操作系统的基本单位是4KB,我们之前说过,不管是inode还是文件的内容,因为 文件 = 属性 + 内容 ,它的文件内容叫做数据块,都是按4KB为单位存的,磁盘把自己划分成了多个数据块,每一个数据块都有地址我们叫做块地址。因此磁盘上的文件不管什么文件,它最终都是4KB的集合!

以32位计算机为例,逻辑上,它的物理内存被划分为了4KB4KB的空间,我们可以把物理内存理解为若个个4KB的数据空间。因此,所谓的文件系统本质上是把磁盘上4KB的内容选择一个物理内存上的4KB空间,把4KB内容载入到指定4KB的空间里,这叫做文件系统的I/O。未来如果想要把物理内存数据刷新到磁盘,必须以4KB的内容整体刷新到物理磁盘当中!因此系统和磁盘I/O的基本单位是4KB!我们把物理内存归并的4KB空间称之为 ---- 页框(页帧)。

如何理解物理内存划分成为4KB的页框?

物理内存的4KB空间里,有些是空的,有些被使用等状态,因此,物理内存的每一个4KB空间,不同的4KB空间保持不同的状态,因此操作系统要管理4KB空间,先描述,再组织!

因此为了描述这个4KB空间,有一个结构体:struct page,它是用来描述每一个物理内存页在系统里都有一个struct page数据结构用来描述当前空间的情况!操作系统内管理所有的page我们用数组来管理:struct page pages1048756;因此对整个物理内存的管理就转化成为对数组的增删查改!我们只需要约定好0下标的page对应第一个物理内存块(4KB),以此类推。因此,从今天开始,我们还需要在系统内部保存物理地址吗?

不需要,如果要访问第一个page,它的起始地址为0,最后一个地址4095,因此只需要知道下标,对应的地址直接可以计算出来。因此物理块起始地址 = 数组下标 * 4KB。因此,我要申请一个内存块,本质是你要申请到struct page得到它在struct page pages的下标,那么物理内存的所有地址我都有了!因此,申请所谓的内存,只需要在数组中申请一个struct page结构即可!

操作系统如何得到所有的物理内存块(page)地址?

只需要得到page数组的起始地址,只需要page数组是全局的即可!

结论:文件,进程和内存之间的关系变成了file,task_struct,page数据结构之间的关系了,即它们被表征成了内核算法了!

虚拟地址空间通过页表与物理内存进行映射的本质:mm_struct + vm_area_struct 通过页表映射到page即可!page数组 -> page -> index(下标) -> 物理page起始地址。

1.2.2页表

①CPU存在一个寄存器叫做EIP寄存器,它表示的是当前正在执行指令的下一条指令地址,因此CPU要读取新指令,从物理内存读进入到CPU内部的所有指令内部包含的地址都叫做虚拟地址,在CPU内部要做虚拟地址到物理的转化,虚拟地址在编址的时候,是32位的连续整数,我们以前认为虚拟地址是整体,但是实际上它并不是整体,它是以10、10、12三部分构成,整体让我们看出来的就是32位的虚拟地址。

②在CPU内部存在CR3寄存器,CR3寄存器会保存当前进程的页表,页表不是一个整体,32位系统采用二级页表,第一张页表叫做页目录,它一共有1024个页表项,第二级页表才是真正的页表,而页目录里页表项存放着是下一级页表所对应的起始地址,后面的页表每一个也都包含1024个页表项,就类似一个多叉树。

虚拟地址转化到物理地址,默认是没有直接转化到字节的。而查页表的过程,只是帮我们查找到你要访问的那一个页框。

因为虚拟地址有32位分为10/10/12,它会先拿着前10位作为整型值直接作为下标找到页目录,映射到要查的页表是哪一个(查页表起始地址),相当于前10位是从页目录表找到具体的页表项;然后拿着中间的10位作为数组下标从页表里查物理页的起始地址,此时就能找到物理内存对应的页框了,此时就知道物理内存的起始地址了,最后拿着虚拟地址的低12位做页内偏移!真正的物理地址(字节) = 页框起始地址 + 页内偏移。(冷知识:2^10 = 1024 = 1KB,2^12 = 4096 = 4KB)其中页框的起始地址是通过查表得到的,页内偏移是通过低12位得到的!

因此一个页目录表有1024个页表,每一个页表要占4KB的空间,因此一个页表大小为1024 * 4KB = 4MB空间,而且这个是进程把所有页表都用满即所有虚拟地址用满的情况下最多才占4MB,因此页表只能更小!

因此,我们创建进程时,只要一个页目录即可,后续才需要申请物理空间!

1.2.3一些细节

细节①:CR3保存的是当前进程页表的基地址,保存的是物理地址,因为如果是虚拟地址还需要通过页表映射;

细节②:虚拟地址划分,这个过程编译器用参与吗?不关心,编译器只需要从全0到全F编址就行;

细节③:为什么做页内偏移的是低12位,而不是高12位?高20位相同的地址,一定是连续存放在一个页框的!因此相同的内容一定在同一个4KB的空间里,即相同属性的代码或数据,在一个4KB数据块里。

细节④:如果我知道任意一个虚拟地址,如何得到我所处的页框?addr & 1111 1111 11 1111 1111 11 0000 0000 0000即可得到页框。

如何得到page结构体?页框号/4KB得到数组下标,因此我们就可以进行虚拟地址到物理地址的转化:虚拟地址 ←------→物理地址。

细节⑤:进程首次加载磁盘块的时候,操作系统会做什么?Ⅰ内存管理申请内存;Ⅱ申请page;Ⅲ得到page在page数组的下标index;Ⅳ得到页框的物理地址;Ⅴ填充页表。

细节⑥:如果我们访问的是一个变量/结构体/数组?所有的变量都只有一个地址,我们拿到的地址是开辟空间的最小字节的(虚拟)地址,那我们页表转化的时候,只能拿到第一个字节的地址啊!所以语言存在一个叫做"类型"的概念,根据类型得到你要访问的有多大,如果是结构体/类/数组,它还会有偏移量,因此在系统层面上访问变量是通过起始地址+偏移量(由类型决定)方式来访问的!

细节⑦:如何理解写时拷贝,缺页中断?操作系统内申请和管理物理内存,是以4KB为单位的,因此写时拷贝是以4KB为单位,且缺页中断也是以4KB为单位!(用空间换时间)

细节⑧:我们用malloc/new申请空间,我怎么可以以1/4/n个字节随意申请,我怎么感觉不到它是以4KB为单位的?

new/malloc底层一定要调用系统调用,调用系统调用也是有成本的,因此为了减少成本,让用户能以任意字节申请,C/C++自己在语言层会有自己的内存管理机制,类似STL的空间配置器,因此我们malloc/new时需要告诉操作系统自己要申请多少字节,而free/delete确不需要告诉要释放多少字节,因为C标准库里有描述内存块的结构体!

因此,我们看到申请字节可以任意申请是因为把操作系统4KB申请空间的过程封装了让我们看到是可以随意申请多少空间,但实际上,操作系统只认4KB。

1.3多线程的实现

要理解线程,需要用代码角度来理解,Linux当中想要创建多线程用到的接口是:pthread_create:

cpp 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

这个函数是库函数,作用是:创建一个线程。

第一个参数thread:线程ID,输出型参数,我们传递一个pthread_t变量给他,它会返回这个线程的id值;第二个参数attr:我们创建线程时设置的属性,如:线程优先级,栈大小等,后续我们线程属性统一设置为NULL,用缺省参数即可;第三个参数是一个参数为void*,返回值为void*的回调函数的函数指针类型,将来创建的线程将会执行该函数;第四个参数是第三个参数即函数指针的参数(void*类型)。返回值:如果线程创建成功就返回0,如果失败就返回错误码。

当你要使用这个方式来创建线程的时候,因为它属于第三方库,不属于C/C++,是Linux系统默认提供的,因此编译时必须要加上pthread库,即-lpthread,之前我们在动态库里讲过,对于不是C语言的库,就需要用-l链接这个库才行!

写下如下代码:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<string>

//新线程执行函数
void* thread_routine(void* args)
{
    //拿到参数
    std::string name = (const char*)args;
    //也不知道做什么就死循环
    while(true)
    {
        std::cout << "我是新线程...,名字:" << name << std::endl;
        sleep(1);
    }
}

int main()
{
    //线程id
    pthread_t tid;
    //创建线程
    pthread_create(&tid,nullptr,thread_routine,(void*)"thread-1");
    //主线程继续往后走
    //现在做不了什么事情就死循环
    while(true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

如果我们要编译,这样编译可能会报错:

bash 复制代码
g++ thread.cc

当然也可能编译正确:

如果编译错误就这样写:

bash 复制代码
g++ thread.cc -lpthread

运行起来有:

如果我们在运行期间新开一台机器,执行以下命令:

bash 复制代码
ps ajx | grep a.out

有:

我们查进程的时候,只查到了一个进程,且二者同时运行。

因此一个单进程,可不可能让两个死循环跑起来?不可能!

一个进程内部有两个不同的执行流,这种用pthread_create创建的叫做线程!

1.4理解资源"划分"

我们现在从可执行程序的角度来理解资源"划分":线程创建好了,①是一个可执行程序,②一个进程,③有几套页表?

因为一个进程只有一套页表,因此就只有一套页表!

一个函数不只有一个地址,函数被编译器编译完,是有一批地址的,这个地址是虚拟地址,而所谓的函数有地址是函数对应的一批地址里面的入口地址称之为函数地址。因此,和上面虚拟地址空间谈论的变量一样,就可以通过基地址+偏移量的方式来访问代码!

进程页表的本质是什么?

是进程看到资源的"窗口",如果没有页表的话,进程访问不了任何资源,因此,谁拥有更多的虚拟地址,谁就拥有更多的物理内存资源!对函数进行编址,让不同的执行流执行不同的函数,函数就是虚拟地址的集合。让不同的线程实现不同的函数,本质是让不同的线程,通过拥有不同区域的虚拟地址,拥有不同的资源!通过函数编译的方式,进行了进程内部的"资源划分"。

1.5Linux内核多线程的实现

操作系统内必定存在多个进程,而每个进程都可能存在多个线程,因此,操作系统内必定存在多个线程,所以需要对线程进行管理。一般来说,管理线程是先描述,再组织。

描述线程的结构体是struct thread control block简称struct TCB,线程控制块。我们还要实现容器化管理线程PCB,调度算法。实际上,这个线程和进程概念上是很像的,因此,设计内核的程序员就把进程的代码进行复用了!

因为进程PCB会指向当前进程的虚拟地址空间,那么我就可以同时创建多个task_struct来执行代码区的一部分,此时的所有进程就能通过虚拟地址空间看到同一份资源,但是经过页表看到的是整个代码的不同区域,此时我不是连线程的代码都不用写了,直接参考进程的不就行了,因此,这就是线程!

结论:Linux内核中,线程的实现是用进程模拟的,复用了进程的代码和结构!

①Linux中,一个线程在进程内部运行,线程在进程内部的虚拟地址空间中运行!

②为什么线程更轻量化?让不同的线程访问虚拟地址空间的一部份资源。如何做?

以代码区为例,让不同的线程执行不同的入口函数即可!

什么叫做进程?

一个进程不仅仅是一个task_struct,而是要在进程创建时创建一个或多个执行流,创建虚拟地址空间,创建页表,加载代码和数据的映射关系,构建页表,进程叫做:承担分配系统资源的基本实体,一个进程就和一个容器一样,它里面可以承装一个或多个执行流。

如何理解历史进程(以前学的进程)?

我们之前讲的进程内部只有一个执行流,只有一个线程的进程,今天我们说的进程,是可以有一个或多个执行流的进程,因此,前面学的进程是现在进程的普通情况!以前学的是单线程进程,现在学的是多线程进程!

因此,现在的进程 = 一个或多个task_struct + 一个虚拟地址空间 + 一个页表 + 代码和数据。

进程向操作系统申请一批资源,然后把进程当做一个容器,内部可以承装多个线程!

站在CPU的角度,它需不需要区分是单线程进程还是多线程进程呢?

不需要,它看到的一个一个task_struct都叫做一个执行流,而这个执行流的粒度一定是 <= 传统的进程的!因此,谈及执行流,在CPU角度,不区分进程线程,统一叫做:轻量级进程!

什么叫做线程?

操作系统调度的基本单位!

①那么Linux的实现和操作系统教材的概念对比?

②Linux操作系统需要有线程的概念吗?

①教材:是CPU调度的基本单位,线程在进程内部运行---概念和要求。而每个操作系统要实现这个概念(标准)就需要进行自己实现,我们刚刚讲的线程实现是Linux线程具体的实现方案,Windows并没有用线程模拟,而是专门定义TCB,调度算法等!因此我们在教材的概念上是学不懂的,因为它没有提供具体的实现方案,只提供指导方法!

②Linux系统里,不存在线程概念,只存在轻量级进程的概念!----- Linux特有!

1.6理解新的进程和线程概念

现实世界分配社会资源的基本单位是什么?

家庭。一个家庭内部有多个家庭成员,每个家庭成员都做着不同的事情,但是大家有共同的目标:把日子过好!虽然我们做着不同的事情,但最终会达到同样的目标,家庭相当于进程,而家庭内部的成员就是线程,让不同的线程做不同的事情但是它们达成的是同一个目标,线程之间是协作关系,达成同一个目标就是进程创建的目标!

一个家庭有多个成员 ---- 进程有多个执行流(线程);之前的只有一个执行流 ---- 一个家庭只有一个成员。因此线程是操作系统调度的基本单位!

怎么证明?

我们的代码执行了两个死循环,是因为有两个PCB,被CPU调度切换并发运行,因此能执行两个死循环!

如果想要看到轻量级进程,要用这个指令:

bash 复制代码
ps -aL | grep a.out

能看到两个执行流:

如果想要看到提示信息需要提取第一行:

我们观察到二者PID相同,代表是同一个进程,TTY相同代表它们两个的终端(显示器)都一样,代表往同一个显示器打印,只有LWP不同。

LWP:light weight process轻量级进程!因此,操作系统看pid,还是看LWP?

是LWP!如果一个进程只有一个线程,那么我们观察发现,PID = LWP,而这个相等的线程叫做主线程,不相等的线程叫做新线程,而不是叫做副线程!因为操作系统调度的基本单位是线程,所以看LWP!

如果想要证明它们两个的pid确实相等,直接多打印一下pid即可,这里不做演示了!

因为Linux中不存在线程的概念,而只存在轻量级进程的概念,因为Linux中没有TCB,只有PCB来实现线程,因此,Linux系统,给用户提供的系统调用,只能提供轻量级进程的系统调用,因此就能理解为什么那个pthread_create接口是外部的,需要自己链接的原因!

创建轻量级进程的系统调用的名字叫做clone:

cpp 复制代码
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, .../* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

这个系统调用太复杂了,我们不使用它,它是写库的人使用的!

clone的作用是:哪个进程调用clone就会在自己的进程地址空间创建task_struct。

实际上pthread_create底层就是调用clone的系统调用,因此操作系统只会提供轻量级进程的系统调用!

但是这样弄的话,Linux就没有线程的概念了,因此Linux的程序员在操作系统之上封装一层库,这个库对clone轻量级进程做系统调用封装它,对用户提供的接口统一转化为线程接口,因此如果想要使用轻量级进程的时候,统一通过库函数来访问,给你提供一整套线程接口,因此就能用线程在Linux使用了!这个库就叫做pthread库,我们把它叫做:原生线程库!----- 任何Linux系统,都会自带这个库!

1.7线程的优缺点

优点:

①创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多,创建进程需要创建进程PCB,还要创建虚拟地址空间、页表、构建映射,而创建一个线程只需要创建一个task_struct即可;

②与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多 :

Ⅰ最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

Ⅱ另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

操作系统在执行进程调度的时候,轻量级进程有task_struct,执行第一个线程的时候,它的CPU寄存器就叫做当前执行流的硬件上下文,那在进行线程切换时,这个硬件上下文需要保存吗?

CPU里寄存器只有一套,但是寄存器里的数据可以有多套,因此寄存器里保存的所有数据都属于当前执行流的上下文,因此硬件上下文要保存,同时只要PCB进行切换,那么它的地址空间也切换了,CPU内部存在的CR3寄存器保存页表对应的基地址,即一个执行流有基地址和CPU的硬件上下文。

进程切换和线程切换最本质的区别是:要不要切换页表的基地址,因为两个task_struct只要属于一个地址空间,页表只要不换,看到的就是当前进程的代码和数据,因此,只要CR3不切换就叫做线程内切换,CR3切换了,就叫做进程间切换,因此CPU在切换的时候,要统计和当前进程相关的所有线程的时间片有没有用完,因此,在CPU角度,它不用严格区分线程和进程,这是一个区分点!

CPU怎么知道一个进程有多少线程,它们的时间片有没有用完怎么知道?

因为每个PCB的pid是相等的(同一个进程的PCB),只要这个进程内所有的线程时间片用完了,只要拿着当前进程的pid就能找到所有的线程了,只有当所有线程把时间片用完才叫做进程把时间片用完了,且每个线程分到的时间片是该进程分到的时间片 / 线程的数量。

因此,线程切换比进程切换要做的工作少有以下几点:

Ⅰ线程切换不用切换CR3寄存器;ⅡTLB->缓存虚拟到物理地址 ---- 线程间切换,不需要更新TLB;ⅢCPU有cache硬件,cache内部缓存的是内存数据,它缓存代码和数据,进程内多线程切换,cache缓存不用更新,但是进程间切换,就需要把cache缓冲区"热起来"

③线程要占用的资源比进程少;

④能充分利用多处理器可并行的数量;

⑤在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务;

⑥计算密集型(涉及到算法的都叫做计算密集型)应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现;

如:10GB的数据排序,如果用单线程去排很麻烦,如果创建多线程去排,每个线程分配一部分数据进行排序,然后归并,但是如果创建多了线程,虽然我们在计算密集型里可以创建多个线程,但一般不要太多,建议创建的线程数是:CPU个数 * 核数,如:2个CPU,4核,就建议创建8个线程。因为计算密集型,大部分操作是在内存中操作,线程创建太多了,它会额外增加操作系统调用线程时的切换线程的成本,因为每个线程都抢的是CPU资源,没必要,还不如让一个线程去做完更好!

⑦I/O密集型(主要是读取文件/网络/键盘/显示器这样的外部设备叫做I/O密集型)应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

I/O密集型创建的线程可以适当多一些,这个影响效率不太大,因为I/O的时间基本是在等待从外设的输入输出,还不如多创建几个线程,效率能提高!

缺点:

①性能缺失:⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指 的是增加了额外的同步和调度开销,⽽可⽤的资源不变。

②健壮性降低:编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护的,如:如果一个线程的代码出问题,导致操作系统发信号给进程,会导致所有的线程都终止了,相当于本来只有一个线程的问题,导致整个进程都运行不了!

③缺乏访问控制:进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。

④编程难度提高:编写与调试⼀个多线程程序⽐单线程程序困难得多(这个是相对的,并不代表是线程的缺点,只要代码没问题,完全不是缺点)。

1.8线程异常

①单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃;

②线程是进程的执⾏分⽀,线程出异常,就类似进程出异常,进⽽触发信号机制,终⽌进程,进程 终⽌,该进程内的所有线程也就随即退出。

1.9线程用途

①合理的使⽤多线程,能提⾼CPU密集型程序的执⾏效率;

②合理的使⽤多线程,能提⾼IO密集型程序的⽤⼾体验(如⽣活中我们⼀边写代码⼀边下载开发⼯ 具,就是多线程运⾏的⼀种表现)。

线程控制内容比较多,所以放到下篇博客进行讲解了!

相关推荐
H.莓飛1 小时前
【数据结构】排序总结与规模实测
linux·服务器·开发语言·数据结构·visual studio·1024程序员节
Zenova EdgeOS1 小时前
Linux 日志实战:用 journalctl 精准检索与治理工业边缘日志
linux·运维·chrome
Dxy12393102161 小时前
Linux 压缩解压相关命令介绍
linux·运维·服务器
eleven_4u1 小时前
QPS如何计算,如何部署架构
java·linux·运维·jvm·中间件
东城居士2 小时前
理解Linux RTC驱动
linux·嵌入式系统
蘑菇小白2 小时前
Linux多线程开发
linux·开发语言·线程
Ruiery3 小时前
Linux 6.6内核内存管理深度解析(十):KSM 同页合并 — 内容去重,把 N 份相同内存合并成 1 份
linux·运维·服务器
Huangjin007_3 小时前
【Linux 系统篇(三十三)】通信(三):System V 共享内存详解
linux·运维·服务器
叼烟扛炮3 小时前
Linux 学习笔记(三十八):从实模式到分页——处理器内存管理的发展史
linux·运维·服务器