Linux进程信号

1.信号的初步认识

1.1生活角度的信号

  1. 红绿灯 狼烟 闹钟 上下课铃 敲门 等等。
  2. 以上这些信号现在可能没发生,但是一定知道当信号发生时该怎么处理。
  3. 经过常年累月的训练,我们的大脑中构建了,信号产生和信号处理的映射。
  4. 当信号产生时,我们可能不会立即处理,因为有优先级更高的事要做,我们会在合适的时候处理信号。
  5. 处理信号的过程:认识信号,识别产生,动作处理。
  6. 进程完成信号处理过程都是内核程序员写的内置特性。

1.2信号的处理

信号处理的方式:
  1. 默认处理
  2. 忽略
  3. 自定义动作

信号处理的动作就是信号捕捉。

信号:外界或其他人或硬件给进程发送的一种异步的事件通知机制。

通知机制:告诉进程什么事情发生了。

异步:多种事件彼此不影响,同时发生。(信号产生时,进程还在做原来的事情)

事件:终止,异常,指令退出。

1.2信号的生命周期

  • 信号保存:当信号产生时,进程还有其他优先级更高的事情要做,所有要把信号保存,等忙完再处理信号。(信号不会被立即处理)

2.信号的产生

2.1见一见信号

  • 1-31:普通信号 34-64:实时信号
  • kill -l罗列出来的信号:2) SIGINT <==> #define SIGINT 2
cpp 复制代码
#include<iostream>
#include<unistd.h>

int main()
{
    while(true)
    {
        std::cout<<"test sign pid:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}
  • 进程是能认识,识别,处理信号的,以上可以看到2号信号的处理动作是终止进程
  • 较大部分的进程收到信号的默认处理动作都是终止进程。

2.2自定义处理动作

  • 更改当前进程对信号处理的动作的函数
  • 参数1:目标信号编号(修改当前进程收到该信号编号时的处理动作)
  • 返回值:一个函数指针,指向修改前信号默认处理动作。
  • 参数2:传递回调函数。(当进程收到signum信号编号时,把默认的信号处理动作改成该回调函数)
  • 对信号的自定义捕捉,只需要在代码的开头处调用一次即可。
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

//signo: signal number
void handle(int signo)
{
    std::cout<<"receive signal:"<<signo<<std::endl;
    //exit(10);
}  

int main()
{
    signal(SIGINT, handle);
    signal( 3, handle);
    while(true)
    {
        std::cout<<"test sign pid:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}
  • ctrl + c : 终止进程,通过键盘给进程发送二号信号,默认处理动作为终止。
  • 信号自定义捕捉,如果回调方法不退出,进程就不会退出.
  • 如果把所有信号都自定义,并且不退出,会出什么问题?
  • 9号和19号信号不可被捕捉,不可被忽略和自定义,9号为管理员信号,默认动作终止进程。
  • 忽略信号参数2传SIG_IGN
  • 默认动作参数2传SIG_DFL

2.3信号产生

  1. 用命令kill产生
  2. 用键盘产生
键盘信号:
  • ctrl+c:向目标进程发送2号信号---默认终止进程
  • ctrl+\:向目标进程发送3号信号---默认终止进程
  • ctrl+z:向目标进程发送19号信号---默认暂停进程

键盘产生信号只能用来控制前台进程,只有前台进程才能获取键盘输入。

bash进程把所有信号都忽略了,但是只要拿到bash的pid通过kill -9也可以杀掉bash进程

如果我们的程序div0,野指针,程序就会崩溃----收到信号

除0----8号信号

代码报错,硬件异常产生信号
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

//signo: signal number
void handle(int signo)
{
    std::cout<<"receive signal:"<<signo<<std::endl;
    sleep(2);
    //exit(0);
}  

int main()
{
    // signal(SIGINT, handle);
    // signal( 3, handle);
    for(int i=1;i<32;i++)
    {
        signal(i, handle);
    }
    while(true)
    {
        std::cout<<"test sign pid:"<<getpid()<<std::endl;
        sleep(2);
        int* p = nullptr;
        *p = 100;

        // int a = 10;
        // a/=0;
    }
    return 0;
}

进程是如何保存信号的?保存到哪?

  • 信号被保存在task_struct结构体中,使用位图结构保存收到的信号。
  • bit位的位置表示信号编号,bit位的内容(1,0)表示是否收到信号。

OS向目标进程发送信号:OS修改目标进程的信号位图

信号的产生方式有很多种,但只有OS才能给进程写信号。

  • div 0:本质是cpu出错
  • 野指针:本质是MMU+页表报错。

本质都是硬件报错,OS是硬件的管理者,所有由操作系统向进程发送信号。

为什么报错会死循环调用回调函数?

  • 进程收到8号信号,用户捕捉了这个信号,没有让进程退出。
  • 那么这个进程有PCB,进程状态等,这个进程还活着,那么这个进程就会被cpu继续调度,每次调度,OS就会发现信号位图的第八位被标记,有报错不能进行,OS就会继续给进程发送8号信号,每调度一次就发一次信号,所以就死循环了。

core dumped是什么意思?

  • 以上两种信号都是终止进程。
  • core dumped就是表示进程退出了,但是多了一步"核心转储"。
  • 我们的进程出现异常导致退出,那么退出的方式都会以core的方式退出。
  • 用户在进程本身没有异常的情况下,强制的将进程退出(2号,9号),就会以term方式退出。
  • 进程自己出异常导致退出,用户最想了解的是进程为什么退出,进程运行到哪一行才导致异常的等数据本来是存在内存中,经过核心转储把进程运行时的异常信息转储到磁盘中
  • core file size为0,云服务器默认把核心转储功能关闭
  • 核心转储功能方便debug
  • bash进程是所有进程的父进程,进程的退出信号和退出码之间有一个bit位为core dump标志位,发生核心转储该位被置1,bash通过进程等待拿到status,就可以分析出是否发生核心转储。
由函数产生信号
cpp 复制代码
#include<iostream>
#include<signal.h>


int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cout<<"Usage: "<<argv[0]<<" <pid> <signo>"<<std::endl;
        return -1;
    }
    pid_t pid=atoi(argv[1]);
    int signo=atoi(argv[2]);
    if(kill(pid,signo)==-1)
    {
        perror("kill");
        return -1;
    }
    return 0;
}
  • 自己给自己发信号
cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

int main()
{
    std::cout<<"pid:"<<getpid()<<std::endl;
    signal(6,[](int signo){
        std::cout<<"received signo:"<<signo<<"pid:"<<getpid()<<std::endl;
    });

    sleep(2);
    abort();
    return 0;
}
  • 给调用进程发送指定信号(6号信号)终止进程
  • 如果在进程代码中捕捉了6号信号并在自定义方法中没有退出进程,最终进程还是会退出的,该信号的自定义动作不是覆盖动作,而是新增动作,默认处理动作还存在,同时进程退出后还会核心转储。
由软件条件产生信号

1.管道

一个进程向管道中写,一个从管道中读数据,当读数据的进程关闭读端,写端还在写,OS就会识别到软件异常,OS就会杀掉写端进程,发送13号信号。

2.alarm

  • 给特定的进程(调用进程)设置闹钟
  • 参数为秒数。
  • 调用函数后seconds秒后发送14号信号。
cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>


static int cnt=0;
void handler(int signo)
{
    std::cout<<"cnt:"<<cnt<<std::endl;
    exit(0);
}

int main()
{
    signal(14,handler);
    alarm(1);
    while(true)
    {
        cnt++;
        //std::cout<<"cnt:"<<cnt<<std::endl;
    }
    return 0;
}
  • 结论:访问外设,IO的效率特别慢
cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>


// static int cnt=0;
void handler(int signo)
{
    std::cout<<"received signo:"<<signo<<"  pid:"<<getpid()<<std::endl;
    int n =alarm(2);
    (void)n;
}

int main()
{
    signal(14,handler);
    alarm(2);
    while(true)
    {
        std::cout<<"processing..."<<std::endl;
        sleep(1);
        //std::cout<<"cnt:"<<cnt<<std::endl;
    }
}
  • 如果一个闹钟还没结束,进程就收到了14号信号,那么下次设置闹钟的返回值是当前闹钟剩余的时间。

3.保存信号

在某些情况下,信号不会立即处理,产生之后,处理之前,就有时间窗口,所以需要进程把信号保存起来。

3.1信号其他常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号,信号只要处于阻塞,该信号就不能被递达。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

3.2信号在内核中的表示

  • 在linux内核进程PCB中存在三张表
  • **pending:**位图结构(一个32位整数),bit位的位置代表信号编号,bit位的内容表示是否收到该信号,操作系统向进程发送信号,就是将特定编号的bit位中的0改为1,在这张表中的信号都处于信号未决状态。
  • **block:**和pending一样的位图结构,bit位的位置:信号编号,bit位的内容:是否阻塞。
  • 相同位置的pending和block表都为1:收到信号,同时该信号被阻塞,永远不会被处理。
  • pending位置为1,block位置为0:收到信号,立即处理该信号
  • block位置为1:不管pending位置为0还是为1,这个信号都不会处理。
  • **handle:**bit位的位置为信号编号,bit位的内容为处理该信号的处理方法。(函数指针)
  • 设置特定编号信号的捕捉,就是修改特定编号位置的handle表的内容
  • handle表中内容为0,表示默认动作,为-1表示忽略
  • 所以在信号还没产生时,进程就已经可以识别和处理信号了
修改进程信号位图表的系统调用
  • 该接口是对block表进行操作,有3中操作方法。
  • 在我们的代码中,我们可以自己建一个sigset_t的变量set,我们自己修改该变量的值
  • 参数1:哪种操作方法
  • 参数2:我们自己创建的位图信息
  • 参数3:输出型参数,再修改block位图前,把位图拷贝一份输出出来给用户。
  • 该系统调用是对pending表进行操作。
  • 参数:输出型参数,把当前进程pending表的01位通过set输出出来。
  • 我们可以通过kill系统调用来修改这个位图
sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集 ,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(SignalMask),这里的"屏蔽"应该理解为阻塞而不是忽略。

信号集操作:

  • sigemptyset:信号集清零
  • sigfillset:全部置1
  • sigaddset:指定信号,添加到信号集。
  • sigdelset:指定的信号,从信号集中删除
  • sigismember:判定一个信号是否在这个集合中。
编码演示:
cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

void PrintPending(sigset_t &pending_set)
{
    for(int i=31;i>0;i--)
    {
        if(sigismember(&pending_set,i))
        {
            std::cout<<"1";
        }
        else
        {
            std::cout<<"0";
        }
    }
    std::cout<<std::endl;
}

void handler(int signo)
{
    std::cout<<"recv a signal:"<<signo<<std::endl;
}

int main()
{
    signal(2,handler);
    //1.屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,2);

    int n =sigprocmask(SIG_SETMASK, &block_set, &old_set);
    (void)n;
    int count =0;
    while(1)
    {
    //2.获取pending信号集
        sigset_t pending_set;
        sigemptyset(&pending_set);
        sigpending(&pending_set);
        PrintPending(pending_set);
        //解除2号信号的屏蔽
        if(count==10)
        {
            std::cout<<"解除2号信号的屏蔽"<<std::endl;
            sigprocmask(SIG_SETMASK, &old_set, nullptr);
        }
        if(count==15)
        {
            abort();
        }
        count++;
        sleep(1);
    }
    
    return 0;
}
  • 一旦我们解除对某个信号的阻塞,那么该信号就会立即被递达。
  • pending位在handler调用之前就清0

4.处理信号

4.1信号何时处理?怎么处理?

当进程调度的时候,从内核态返回用户态的时候,会进行信号的检测和处理。

  • 在执行信号捕捉时,OS以用户态的身份执行handler方法(handler处于[ 0 , 3GB ]的虚拟地址空间中)
  • 由于内核态的权限比较高,如果以内核态的身份去执行handler方法,可能会有一些非法代码被执行:execl("rm","/");
  • 信号处理是从内核态返回用户态的时候,进行信号的检测和处理。

信号捕捉的流程

  1. 用户态执行代码
  2. 通过系统调用进入内核态执行系统调用
  3. 系统调用结束返回用户态之前,检查信号集
  4. 如果忽略信号直接返回用户态继续执行代码,本流程结束;如果是默认终止进程则在内核态直接终止,本流程结束;如果用户自定义处理方法,则会先返回用户态去执行自定义方法。
  5. 自定义方法执行结束再返回内核态,继续在中断的位置执行内核代码。
  6. 内核代码(系统调用)执行结束后返回用户态。

4.2操作系统是怎么运行的

硬件中断
  • 进程由于scanf等操作等待外设输入时,当外设准备就绪,外设会触发中断(高电平),通过中断控制器,向cpu触发中断,通知cpu,外设已经准备好
  • 当cpu正在执行代码时,由键盘或其他外设,向cpu产生异步产生中断
  • 一台电脑的外外设有很多,外设出发中断先通知中断控制器,中断控制器会把"谁触发的中断","触发中断是为了让cpu做什么"这些信息发送给cpu

外设中断处理动作:中断向量表

  • 我们可以把中断向量表理解成一个函数指针数组。
  • 每一个外设都有自己的**中断号,**中断控制器会把中断号发送给cpu。
  • cpu通过某种方法将中断号映射为中断向量表的下标,通过下标找到外设的中断处理方法。
  • cpu会先保存自身寄存器中的数据,然后再执行中断处理方法,执行结束后,再将保存的数据加载回寄存器中,继续执行。

在计算机中,其实是现有硬件中断,由硬件完成处理,后来,人们发现,进程也需要类似的机制,就发明了信号机制,信号的机制就是使用纯软件的机制来完成特定任务的处理,信号,硬件中断,原理类似,本质不同。

中断向量表就是操作系统的一部分,启动就加载到内存中了。
通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询

时钟中断

一、先从通俗的比喻理解核心概念

我们可以把整个计算机系统比作一家工厂:

  • CPU:工厂里唯一的工人,负责干活(执行指令)。
  • 应用程序:工人手头的具体任务(比如生产零件 A、组装产品 B)。
  • 操作系统内核:工厂的管理员,负责分配任务、监督进度、处理突发情况。
  • 硬件定时器:工厂墙上的闹钟,能按固定时间(比如每分钟)响铃。
  • 时钟中断:闹钟响铃这个 "提醒信号",会强制工人停下手里的活,先听管理员安排。

没有时钟中断时,工人(CPU)会一直干一个任务直到完成,管理员(OS)完全插不上手;有了时钟中断,管理员就能定期 "打断" 工人,检查任务进度、切换任务,实现多任务并发。

二、时钟中断与硬件定时触发的底层原理(由浅入深)

1. 硬件定时器的工作方式

硬件定时器是 CPU 外部的一个独立硬件组件(比如 x86 架构的 PIT/APIC 定时器、ARM 的 SysTick),核心特性:

  • 可以由操作系统内核初始化配置:设置定时周期(比如 10ms,即 100Hz,意味着每秒触发 100 次中断)。
  • 定时器会根据配置的周期,从预设值开始倒计时,计数到 0 时,会向 CPU 的中断控制器(如 APIC、GIC)发送一个 "中断请求(IRQ)"。
  • 这个过程完全由硬件完成,不占用 CPU 的运算资源。

2. CPU 响应中断的完整流程

CPU 执行指令的过程中,会周期性检查中断控制器(比如每条指令执行完、或每个时钟周期末尾),当检测到时钟中断请求时,会按以下步骤切换到操作系统内核:

3. 操作系统内核在时钟中断里做什么?

时钟中断触发后,CPU 会执行内核预设的 "时钟中断处理程序",核心工作包括:

  • 更新系统时间 :比如累计系统运行的毫秒数,这是datetime()等函数的底层来源。
  • 进程时间片管理:如果当前运行的进程用完了分配的时间片(比如 10ms),就标记 "需要调度"。
  • 触发进程调度:内核调度器会被唤醒,从就绪队列里选一个新进程,让 CPU 切换去执行它(这就是多任务的核心)。
  • 处理定时任务 :比如超时的网络连接、定时信号(alarm()函数)、延迟执行的内核任务。

详细解释:

操作系统给每个进程分配的时间片 ,通常等于若干个时钟中断周期(比如 10 个 10ms 周期 = 100ms 时间片)。时钟中断是周期性的 "检查点",而不是进程执行的 "启停开关"。

  • 每次时钟中断触发时,内核会减少当前进程的剩余时间片;
  • 只有当剩余时间片减到 0 时,内核才会触发进程调度,切换到就绪队列的下一个进程;
  • 如果当前进程在时间片用完前就主动放弃 CPU(比如发起 IO 请求、调用sleep()),内核会
  • 立刻调度新进程,不用等到下一个时钟中断。

更准确的表述是:时钟中断让 CPU从执行用户进程的代码,切换到执行操作系统内核的代码

  • CPU 的核心是 "执行指令流",用户进程和内核本质上是不同的指令流;
  • 中断发生时,CPU 会保存当前进程的执行上下文(寄存器、程序计数器等),然后跳转到内核的时钟中断处理函数执行;
  • 内核处理完中断(更新时间、检查时间片、决定是否调度)后,会恢复某个进程的上下文,让 CPU 继续执行该进程的指令。

完整逻辑链条

  1. 硬件定时器按配置周期(如 10ms)向中断控制器发送请求 → 中断控制器向 CPU 发中断信号。
  2. CPU 暂停当前执行的用户进程,保存上下文,切换到内核态,执行时钟中断处理函数
  3. 内核做三件事:更新系统时间 → 减少当前进程剩余时间片 → 判断是否需要调度。
    • 不需要调度(时间片没用完,且无更高优先级进程):恢复原进程上下文,CPU 继续执行原进程。
    • 需要调度(时间片用完 / 有高优先级进程就绪):运行调度器,选择就绪队列中的下一个进程,恢复其上下文,CPU 执行新进程。
  4. 重复步骤 1-3,周而复始。

这个过程就像老师每隔 10 分钟(时钟中断)巡视一次教室

  • 若学生 A 还在写作业且没超时 → 让他继续写;
  • 若学生 A 作业时间到了 → 让学生 B 接着写;
  • 若学生 A 提前写完举手 → 老师立刻让学生 B 开始,不用等下一次巡视。

每次时钟中断触发时,CPU 一定会在用户态和内核态之间完成一次切换

白话理解:

时钟中断是纯硬件来完成,如果此时cpu正处于用户态执行进程,cpu收到时钟中断后,会现将自身寄存器的进程上下文保存在栈中,然后cpu去执行内核代码(OS),这一过程就是从用户态转到内核态,OS会减少当前cpu正在执行的进程的时间片,如果该进程时间片未减少到0,则继续执行该进程,如果时间片刚好减少到0,则触发进程调度cpu回到用户态执行下一个进程,每次进行时钟中断cpu都会在内核态和用户态之间来回切换。

时间片:

操作系统计算时间:

内核中存在一个全局变量:long long tickts=0;//每次时钟中断++。

电脑在关机状态下会有一个纽扣电池给计时设备供电,这个全局变量即使是在关机状态下也会按照时钟中断的规则计数。

所以电脑每次开机都知道当前是什么时间,这个全局变量也就是时间戳。

时间片就是一个计数器,每次触发时钟中断,计数器--。

cpu主频:

时钟中断的频率由外部时钟源决定,且受 CPU 主频的承载能力约束,但时钟中断频率≠CPU 主频

结论:

操作系统是在时钟源的催促下,以中断的方式,来进行对应的时钟处理

软中断

软中断(Softirq)是操作系统层面的一种异步事件处理机制 ,和硬件中断相对应,它不是由外部硬件设备触发,而是由软件自身主动发起硬件中断处理程序触发,用于解决硬件中断处理耗时过长、影响系统响应性的问题。

一、软中断的核心定位:硬件中断的 "后续处理者"

硬件中断 → 软中断 的执行链路:

  • 硬件中断触发 :比如网卡收到数据、硬盘完成读写,硬件会向 CPU 发送硬中断请求
  • 硬中断处理(顶半部) :CPU 暂停当前任务,执行硬中断的顶半部处理程序 ------ 这部分代码必须极短、极快,只做最核心的工作(比如把网卡数据读到内存缓冲区、标记数据就绪),然后立刻结束硬中断,恢复被打断的任务。
  • 软中断触发(底半部) :顶半部处理完成后,会主动触发一个软中断,把剩下的耗时工作(比如解析数据、通知应用程序取数据)交给软中断处理。
  • 软中断处理(底半部) :操作系统会在CPU 空闲时(或按照调度规则)执行软中断的处理逻辑,完成剩余工作。

简单说:硬中断负责 "应急响应",软中断负责 "后续收尾",这样能最大程度减少硬中断对 CPU 的占用时间,避免系统卡顿。

软中断的典型应用场景

  1. 网络数据处理:这是软中断最核心的场景。网卡收到数据包后,硬中断顶半部仅把数据拷贝到内存,软中断则负责校验数据、拆分协议包、把数据转发给对应应用(比如浏览器、游戏客户端)。
  2. 磁盘 IO 处理:硬盘完成读写后,硬中断标记数据就绪,软中断负责将数据同步到文件系统、通知应用程序 "数据已准备好"。
  3. 定时器与任务调度:操作系统的时钟软中断(由时钟硬中断触发),用于更新系统时间、调度进程切换、计算进程 CPU 占用率。
  4. 内核事件通知:比如进程间通信(IPC)、信号量唤醒等,都可以通过软中断实现异步通知。

C++/ 游戏场景的关联

  1. 游戏网络卡顿的潜在原因 :如果游戏服务器 / 客户端的网络软中断处理效率低(比如数据解析逻辑臃肿),会导致网卡数据堆积,出现延迟升高、丢包等问题。
  2. CPU 主频对软中断的影响:高主频 CPU 能更快执行软中断处理程序,减少软中断队列的等待时间,提升网络、磁盘的响应速度,这也是高主频 CPU 对游戏帧率稳定性有帮助的原因之一。
  3. Linux 下的软中断查看 :在 Linux 系统中,你可以用 cat /proc/softirqs 命令查看各类软中断的触发次数,用 top 命令的 si 列查看软中断占用的 CPU 百分比 ------ 如果 si 数值过高,往往意味着网络或磁盘 IO 压力过大。
cpp 复制代码
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
// 调度程序的初始化⼦程序。
void sched_init(void)
{
...
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
}
_system_call:
cmp eax,nr_system_calls-1 ;// 调⽤号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
push ds ;// 保存原段寄存器值。
push es
push fs
push edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
push ecx ;// push %ebx,%ecx,%edx as parameters
push ebx ;// to the system call
mov edx,10h ;// set up ds,es to kernel space
mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov es,dx
mov edx,17h ;// fs points to local data space
mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72 个
;// 系统调⽤C 处理函数的地址数组表。
call [_sys_call_table+eax*4]
push eax ;// 把系统调⽤号⼊栈。
mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程
序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
cmp dword ptr [state+eax],0 ;// state
jne reschedule
cmp dword ptr [counter+eax],0 ;// counter
je reschedule
;// 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:

中断处理流程

  1. 用户态执行int 0x80触发中断,CPU 切换到内核态;
  2. 中断处理程序从eax寄存器取出系统调用号;
  3. 检查调用号合法性后,从sys_call_table取出对应函数指针;
  4. 传递寄存器参数并执行系统调用函数;
  5. 将返回值存入eax,返回用户态。

系统调用号就是数组下标。

sys_call_tableint 0x80中断的核心跳转表,索引对应系统调用号,值为处理函数指针,系统调用表。

缺页中断,内存碎片处理,除0野指针错误都是什么,它们又有什么联系?

这三个概念都属于内存管理与程序运行时错误 的范畴,分别对应内存访问异常、内存分配效率问题、代码逻辑 / 运行时错误,三者的联系在于:内存管理机制的缺陷或代码错误,可能会连锁触发这些问题

1. 缺页中断(Page Fault)

缺页中断是操作系统的正常内存管理机制,属于一种特殊的中断,不是程序错误。

  • 触发场景 现代操作系统使用虚拟内存 机制,程序运行时只需要把部分代码 / 数据加载到物理内存,其余部分保存在磁盘(交换分区 / 文件)。当程序访问的虚拟内存页不在物理内存中时,CPU 会触发缺页中断。
  • 处理流程
    1. 操作系统中断当前进程,检查虚拟页的状态:
      • 若该页只是未加载(如程序刚启动、数据未被访问过)→ 执行页面调入,从磁盘把页加载到物理内存,更新页表,然后恢复进程执行;
      • 若该页是非法地址(如访问超出进程地址空间的内存)→ 升级为段错误,终止进程。
  • 核心性质正常的缺页中断是提升内存利用率的关键,只有 "非法缺页" 才是错误。

内存碎片处理(Memory Fragmentation Handling)

内存碎片是内存分配后产生的 "空闲但无法利用的小内存块",处理内存碎片是提升内存利用率的优化手段

  • 碎片的分类
    • 内部碎片 :分配的内存块大于程序实际需求,多余的部分无法被利用。比如用malloc(10)申请 10 字节,但内存分配器按最小粒度(如 16 字节)分配,多出来的 6 字节就是内部碎片。常见于固定大小的内存分配(如分页机制)。
    • 外部碎片:物理内存中存在多个不连续的空闲小内存块,总和足够分配一个大内存,但单个块都满足不了需求。比如物理内存有 3 个空闲块:2KB、3KB、4KB,程序需要申请 8KB → 无法分配,这就是外部碎片。常见于分段分配或动态内存分配。
  • 处理方法
    • 内部碎片:优化内存分配粒度(如使用 slab 分配器)、按需分配。
    • 外部碎片:
      1. 内存紧凑(Compaction):操作系统移动已分配的内存块,将空闲块合并成连续的大内存块(代价是消耗 CPU 资源);
      2. 分页机制:虚拟内存的分页可以天然规避外部碎片(物理内存页无需连续,靠页表映射);
      3. 分区分配算法:如伙伴系统(Buddy System),将内存块按 2 的幂次划分,减少碎片。

除 0 / 野指针错误(Division by Zero / Wild Pointer Error)

这两个是程序运行时的致命错误,直接导致程序崩溃,属于代码逻辑或内存操作错误。

三者的核心联系

  1. 内存碎片 → 间接触发缺页中断
  • 当外部碎片严重时,物理内存中没有足够大的连续块,操作系统无法分配足够的物理页给程序,只能将部分物理页换出到磁盘(交换分区)。
  • 程序后续访问这些被换出的页时,会频繁触发缺页中断 ,导致缺页率飙升
  • 后果:大量时间消耗在 "磁盘与内存的页面交换" 上,系统进入 ** 抖动(Thrashing)** 状态,CPU 利用率极低,程序运行卡顿。
  1. 野指针错误 → 与缺页中断的关联

野指针错误的本质是非法缺页中断

  • 合法缺页中断:访问的虚拟地址有效,但物理页未加载 → 系统正常处理;
  • 野指针访问:访问的虚拟地址超出进程地址空间,或指向已释放的内存 → 触发缺页中断后,系统检查页表发现地址非法 → 升级为段错误,终止进程。
  • 总结:野指针错误是缺页中断的 "异常分支" 结果
  1. 内存碎片 → 加剧野指针 / 除 0 错误的排查难度
  • 内存碎片严重时,内存分配器可能返回 "不稳定" 的内存地址(如靠近其他进程的地址空间边界),此时若程序存在指针越界,更容易触发段错误。
  • 另外,碎片导致内存分配失败(如malloc返回 NULL),若程序未检查返回值,直接使用该 NULL 指针 → 触发野指针错误;或分配失败后程序逻辑混乱,间接引发除 0 错误(如用分配失败的返回值作为除数)。

三者的底层共性

都依赖操作系统的中断机制处理:

  • 缺页中断 → 内存管理子系统处理;
  • 除 0 错误 → 算术异常中断处理;
  • 野指针 → 非法缺页中断升级为段错误处理。没有中断机制,操作系统无法感知和处理这些内存相关的异常

通俗类比

可以把物理内存比作停车场 ,程序比作车主

  • 缺页中断:车主开车到停车场,发现自己的车位(虚拟页)被临时占用 → 保安(操作系统)帮忙协调,把车停到空闲车位,记录位置(更新页表);若车主找的是不存在的车位 → 保安直接赶人(段错误)。
  • 内存碎片:停车场里有很多零散的空位,但都停不下一辆大车 → 保安要么把零散空位合并(内存紧凑),要么引导大车停到临时车位(交换分区)。
  • 野指针 / 除 0 错误:车主认错了车位号(野指针),强行停车 → 保安报警;车主开车时操作失误撞墙(除 0)→ 直接拖车。

操作系统是一个基于中断处理的软件集合。

4.2用户态&内核态

用户态:进程执行代码时,访问数据,在访问[ 0 , 3GB]地址空间的时候,就是在访问用户自己的代码,自己的数据。

内核态:在访问[ 3GB , 4GB ]地址空间的时候,就是访问操作系统的过程。

内核态的权限级别更高。

4.2.1再谈虚拟地址空间

  • 进程的所有系统调用,都是在自己的虚拟地址空间内完成。
  • 每一个进程有自己的一套用户级页表,但是内核级页表只有一份,所有进程共用一份。
  • 每一个进程都有自己的内核虚拟地址空间,但是所有进程内核虚拟地址空间经过内核页表映射会指向同一个物理内存,内核代码在物理内存中只存在一份,并且供所有进程使用。
  • 任何进程在任何时候,在进行系统调用时,都能找到内核代码。

但是直接让用户访问[ 3GB , 4GB ]的内核虚拟地址空间,具有安全风险。

为了防止用户直接使用指针访问内核数据,在系统层面上就需要设置两种权限级别:用户态,内核态。

进程只要处于用户态,进程就只能访问用户虚拟地址空间[ 0 , 3GB]。

  • 进程内部包含了大量的寄存器,这些寄存器就是进程的硬件上下文。
  • 用户态和内核态需要硬件级支持,

4.2.2解释内核态和用户态

  • 描述进程处于用户态还是内核态,但内核态和用户态是cpu的两种执行级别。
  • 在cpu内会存在一些寄存器,这些寄存器内会有专门的bit位,来描述cpu的工作模式。
  • CS寄存器的最低两个标志位 ->这两个bit位,只有两种取值-> 00 , 11 -> 0 ,3 ;0 表示内核态,3 表示用户态。
  • 这两个标志位被称为CPL
  • 当用户进行系统调用要陷入到内核时,系统调用除了要写寄存器eax(调用号),还要将CS寄存器的低两位该为00,此时cpu才处于内核态,才允许用户通过中断的方式唤醒OS去执行系统调用。

在内核级页表中存在DPL标志位:取值0 | 3

DPL标志位为0时表示内核态才能访问该物理内存

  • 当进程拿着虚拟地址想要访问内核代码时,OS会对比CPL和DPL,判断是否相等,如果相等则允许访问,不相等则mmu报错。

4.2.3使用C代码模拟操作系统等待中断

cpp 复制代码
#include <iostream>
#include <vector>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int current=0;

class task_struct
{
private:
	int pid;
	int status;
	int counter;
public:
	task_struct(int p):pid(p), counter(5)
	{}
	void desc()
	{
		counter--;
	}
	void ResetCounter()
	{
		counter = 5;
	}
	int Pid()
	{
		return pid;
	}
	bool Expired()
	{
		return counter<=0;
	}
	void run()
	{
		std::cout << "process " << pid << "running" << std::endl;
	}
	~task_struct(){}
};

std::vector<task_struct> tasks;

void do_timer(int signo)
{
	tasks[current].desc();
	if(tasks[current].Expired())
	{
		std::cout << tasks[current].Pid() << "过期了,重新选择进行调度" << std::endl;
		//选择一个进程运行
		current = rand()%tasks.size();
		tasks[current].ResetCounter();
	}
	else{
		tasks[current].run();
	}


	//reset
	alarm(1);
}

int main()
{
	alarm(1);
	signal(SIGALRM, do_timer);
	srand(time(nullptr));

	
	tasks.emplace_back(1);
	tasks.emplace_back(2);
	tasks.emplace_back(3);
	tasks.emplace_back(4);
	tasks.emplace_back(5);



	for(;;)
	{
		// printf("OS 被中断唤醒\n");
		pause(); // 暂停
	}
}

4.3sigaction

  • 检查和改变一个信号的处理动作
  • 参数1:改变哪一个信号的处理方法。
  • 返回值:成功返回0 失败返回-1
  • 结构体中的第一个成员变量就是信号的处理函数指针。
cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

void handler(int signo)
{
    std::cout << "Received signal: " << signo << std::endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    sigaction(SIGINT, &act, &oact);

    while(true)
    {
        std::cout << "Running..." << std::endl;
        sleep(1);
    }
}
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

void handler(int signo)
{
    std::cout << "Received signal: " << signo << std::endl;
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        for(int i=31;i>=1;i--)
        {
            if(sigismember(&pending,i))
            {
                std::cout<<"1";
            }
            else
            {
                std::cout<<"0";
            }
        }
        std::cout<<std::endl;
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    sigaction(SIGINT, &act, &oact);

    while(true)
    {
        std::cout << "Running..." << std::endl;
        sleep(1);
    }
}

屏蔽3,4,5

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

void handler(int signo)
{
    std::cout << "Received signal: " << signo << std::endl;
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        for(int i=31;i>=1;i--)
        {
            if(sigismember(&pending,i))
            {
                std::cout<<"1";
            }
            else
            {
                std::cout<<"0";
            }
        }
        std::cout<<std::endl;
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&(act.sa_mask));
    sigaddset(&(act.sa_mask), 3);
    sigaddset(&(act.sa_mask), 4);
    sigaddset(&(act.sa_mask), 5);

    sigaction(SIGINT, &act, &oact);

    while(true)
    {
        std::cout << "Running..." << std::endl;
        sleep(1);
    }
}

5.可重入函数

  • 上图在插入node1时,在执行head=p之前,进程收到信号,跳转到信号处理函数,而处理函数也需要插入节点,最终导致4号图的出现,node2节点出现了丢失问题,导致内存泄露。
  • 这种情况被称为insert函数被重入了。
  • insert函数被重入后,导致代码出现问题,所以称insert函数为不可重入函数。反之为可重入

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

6.volatile

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

volatile int flag = 0;
//int flag = 0;


void change(int signo)
{
    flag = 1;
    printf("change flag : 0 -> 1\n");
}

int main()
{
    // register int  x = 10;
    signal(2, change);

    while(!flag); // flag只会被检测,不会被修改!
    printf("进程正常退出\n");

    return 0;
}
  • 在添加volatile之前,我们的代码对flag只做了检测不会做修改,对信号的自定义处理中修改了,但是收到信号概论比较小,编译器就把flag认为成了常量,把flag做优化将其存入eax寄存器中。
  • cpu读取寄存器比读取内存更快,所以即使后续收到2号信号,修改了flag,修改的也是内存中的flag,寄存器中还是0。
  • 所以即使收到2号信号,还是会继续死循环,进程不会退出。
  • volatile:保持内存的可见性,防止编译器的过度优化
  • 添加volatile后,cpu每次检测就会读取内存中的flag了

7.SIGCHLD信号---17号

  • 父进程创建子进程,子进程退出后,子进程会向父进程发送17号信号
  • 类似于硬件准备好,向cpu发送硬件中断。
  • 17号信号默认动作是被忽略。
  • 父进程做自己的事情,只有子进程退出发送信号的那一刻才会开始等待子进程,不浪费父进程的时间。
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


void handler(int signo)
{
    printf("父进程获取信号: %d, pid: %d\n", signo, getpid());
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(-1, &status, 0);
        if(ret < 0)
            break;
    }
    printf("status code: %d\n", WEXITSTATUS(status));
}

int main()
{
    signal(SIGCHLD, handler);

    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        printf("子进程退出, pid: %d\n", getpid());
        sleep(4);
        exit(10);
    }

    while (1)
    {
        printf("父进程在运行: Pid: %d\n", getpid());
        sleep(1);
    }

    return 0;
}
相关推荐
无垠的广袤2 小时前
【工业树莓派 CM0 NANO 单板计算机】YOLO26 部署方案
linux·python·opencv·yolo·树莓派·目标识别
宇钶宇夕2 小时前
CoDeSys入门实战一起学习(九):CoDeSys库文件实操指南——安装、调用与版本管理
运维·自动化·软件工程
txinyu的博客2 小时前
TCP 队头阻塞问题
服务器·网络·tcp/ip
皮蛋sol周2 小时前
嵌入式学习数据结构(二)双向链表 内核链表
linux·数据结构·学习·嵌入式·arm·双向链表
Sleepy MargulisItG2 小时前
【Linux网络编程】网络层协议:IP
linux·网络·tcp/ip
叠叠乐3 小时前
移动家庭云电脑linux docker 容器登陆移动家庭云电脑
linux·运维·docker
I_Jln.3 小时前
Docker:快速构建、运行、管理应用的工具
运维·docker·容器
资料库013 小时前
LVS、Nginx、HAProxy核心区别是什么?
运维·nginx·lvs
Volunteer Technology3 小时前
Centos7安装python和jupyter
linux·python·jupyter