进程信号(Linux)

进程信号

  • 信号入门
    • 身边的信号
    • 进程信号
  • 产生信号
    • 终端按键产生信号
    • 调用系统函数向目标进程发信号
      • kill
      • raise
      • abort
    • 硬件异常产生信号
    • 由软件条件产生信号
  • 阻塞信号
    • 信号其他相关常见概念
    • 在内核中的表示
    • sigset_t
    • 信号集操作函数
      • sigprocmask
      • sigpending
  • 捕捉信号
    • 内核如何实现信号的捕捉
    • sigaction

信号入门

信号分为四个阶段:

  1. 预备
  2. 产生
  3. 保存
  4. 处理

身边的信号

用一个简单的栗子来解释信号的四个阶段:
当我们过马路遇到红绿灯时,首先我们是能够识别红绿灯的(色盲除外),识别包含两个重要的因素:认识,并且能够产生对应的行为;可是我们为什么红绿灯呢?一定是有人所教育的,可能是在学校里面所学习的,亦或被家人所教育的,这其实就是信号的预备阶段;变绿灯时,我们不一定要立刻就过马路,如果此时我们有更重要的事情要处理,我们就会选择等待下一次,变灯其实就是产生信号,选择等待下一次就是信号的处理,在信号产生和处理之间还存在着信号的保存,也就是信号需要被记住;信号的处理方式也可以分为三种:默认动作,就是红灯停绿灯行,自定义动作,例如变绿灯之后等上几秒钟再过马路,忽略,不过马路

进程信号

将上面的概念,迁移至进程中
首先,我们有个共识:信号是由操作系统发给进程的;进程认识信号,并且还会做出对应的动作;并不是信号一产生,进程会立刻处理信号,所以进程本身必须有保存信号的能力;进程处理信号的方式也是三种:默认,自定义,忽略
,处理信号也称信号被捕捉

信号集,只学习[1,31]普通信号
在这里插入图片描述

这里再理解一个概念:既然进程能够保存信号,那么应该保存在哪里呢???
其实不难想象,信号是保存在PCB中的,结构体中存在一个属性是用来保存信号的
在这里插入图片描述
比特位的位置,代表信号编号;比特位的内容,代表是否收到信号,0表示没有,1表示有;所以发送信号的本质就是修改 PCB信号位图,由于 PCB是内核维护的数据结构对象,操作系统又是进程的管理者,所以无论发生什么信号,本质都是操作系统向进程发送的信号,操作系统必须提供发送信号和处理信号相关的系统调用

产生信号

终端按键产生信号

举个栗子,观察进程中的信号

int main()
{
    while(true)
    {
        cout<<"我是一个进程"<<endl;
        sleep(2);
    }
}

在这里插入图片描述
热键ctrl+c能够将进程终止,本质是操作系统将其解释为2号信号SIGINT

在这里插入图片描述

介绍一个函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  1. signum:信号编号
  2. handler:函数指针,将信号自定义处理

修改上面的栗子

void handler(int signo)
{
    cout<<"进程捕捉到一个信号,信号编号:"<<signo<<endl;
}

int main()
{
    signal(2,handler);
    while(true)
    {
        cout<<"我是一个进程"<<getpid()<<endl;
        sleep(2);
    }
}

在这里插入图片描述

虽然设置了 signal函数,但是进程还是正常打印了四次,因为没有捕捉到信号, handker函数并没有被调用;当输入热键时,信号被捕捉,由于处理方式是自定义的,所以进程没有直接退出

调用系统函数向目标进程发信号

kill

int kill(pid_t pid, int sig);
  1. pid:正在运行的进程的pid
  2. sig:将信号sig发送给目标进程;可以是任意信号

实例如下:
mysignal.cpp

void Usage(const string&proc)
{
    cout<<"\nUsage "<<proc<<" pid signo\n"<<endl;
}
int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    pid_t pid=atoi(argv[1]);
    int signo=atoi(argv[2]);
    int n=kill(pid,signo);
    if(n!=0)
    {
        perror("kill");
    }
    return 0;
}

mytest.cpp

int main()
{
    while(true)
    {
        cout<<"我是一个正在运行的进程,pid: "<<getpid()<<endl;
        sleep(1);
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

19号信号可以将进程停下来

raise

int raise(int sig);

将信号sig直接发送给进程(自己)

int main()
{
    int cnt=0;
    while(cnt<=10)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        sleep(1);
        if(cnt>=5)
        {
            raise(9);
        }
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

9号信号直接将进程杀死

abort

void abort(void);

给进程(自己)发送指定信号,终止进程

int main()
{
    int cnt=0;
    while(cnt<=10)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        sleep(1);
        if(cnt>=5)
        {
            abort();
        }
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

发送的指定信号其实是6号信号

硬件异常产生信号

信号的产生不一定是用户显示地发送

观察下列代码:

int main()
{
    while(true)
    {
        cout<<"我正在运行中..."<<endl;
        sleep(1);
        int a=10;
        a/=0;
    }
}

在这里插入图片描述

程序运行的结果是溢出,为什么进行a/=0会终止进程,进程终止是收到操作系统发送的信号,具体是怎么做的呢?
在这里插入图片描述
在CPU中进行计算,将计算结果存放到寄存器中,由于a/=0计算的结果溢出,此时状态寄存器将溢出标记位记为1,由于操作系统是管理软硬件资源的,CPU运算异常会被其得知,然后将8号信号发送给进程,让进程终止

验证如下:

void catchSIG(int signo)
{
    cout<<"获取一个信号,信号编号是: "<<signo<<endl;
}

int main()
{
    signal(SIGFPE,catchSIG);
    while(true)
    {
        cout<<"我正在运行中..."<<endl;
        sleep(1);
        int a=10;
        a/=0;
    }
}

在这里插入图片描述

信号被捕获了,可是进程没有直接退出;收到信号,不一定会让进程退出,既然进程没有退出,就还有可能再次被获取

CPU内部地寄存器只有一份,寄存器中的内存保存地是进程的上下文;当进程被切换时,状态寄存器就会被保存和恢复,每一次的恢复,操作系统都会识别到CPU内部的状态寄存器的溢出标志位是1,便会向进程发送8号信号

由软件条件产生信号

在上一章的管道中,如果关闭管道一端读端,进程会收到13信号,结束进程,这个信号是在软件条件下产生的;这里介绍另一个在软件条件下产生的信号

unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告知内核在seconds秒之后给当前进程发送SIGALRM信号,默认处理是终止当前进程

代码实现:

int main()
{
    int cnt=0;
    alarm(1);
    while(true)
    {
        cout<<"cnt: "<<cnt++<<endl;
    }
}

在这里插入图片描述

为什么说alarm函数就是软件条件下的产生的信息呢???

闹钟其实就是由软件实现的,任何一个进程,都可以通过alarm系统调用在内核中设置闹钟,操作系统内可能会存在着很多的闹钟,操作系统为了管理这些闹钟,需要先描述再组织

先描述
在这里插入图片描述

再描述
在这里插入图片描述

操作系统会周期性地检查这些闹钟;超时时,操作系统会发送信号SIGALARM给进程,也就是调用alarm.p,进而终止进程

最后还有一点,进程退出时的核心转储
观察代码

int main()
{
    int a[10];
    a[1000]=10;
    return 0;
}

在这里插入图片描述
在这里插入图片描述

程序发生了段错误,并且进程是 core退出,在上面还有信号是 term退出,这种退出称为正常退出; core退出时,程序还会做一些其他操作

云服务器上 core退出时,看不到明显现象,默认关闭了 core file
在这里插入图片描述

自己可以打开云服务器中的 core file

在这里插入图片描述

再次运行程序

在这里插入图片描述

段错误后面出现了一个新的内容core dumped也就是核心存储:当进程出现异常时,操作系统会将进程在对应时刻有效的数据转储到磁盘中,运行之后还多出来一个文件core.2545,后面的编号就是引起问题进程的pid

核心转储存在的意义是为了调试,更方便用户检查进程崩溃的原因
实操一下:
在这里插入图片描述

总结

  1. 以上所有的信号产生都是由操作系统来执行,因为操作系统是管理软硬件资源的
  2. 信号的处理并不是立刻,而是在适合的时候
  3. 信号没有被立刻处理,信号会被暂时存放在PCB中
  4. 进程在收到信号之前,就已经知道该如何处理
  5. 操作系统向进程发送信号,其实就是修改PCB中的位图

阻塞信号

信号其他相关常见概念

  1. 实际执行信号的处理动作称为信号递达
  2. 信号从产生到递达之间的状态称为信号未决
  3. 进程可以选择阻塞某个信号
  4. 被阻塞的信号产生时将保存在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  5. 阻塞和忽略是不同的,信号只要被阻塞就不会递达,忽略本质上就是递达之后选择的一种处理动作

在内核中的表示

在这里插入图片描述

每个进程都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作;信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志;如果一个信号没有被产生,并不妨碍它可以先被阻塞

sigset_t

每个信号只有一个比特位,非0即1,不记录该信号产生了多少次,阻塞标志亦是于此;未决和阻塞都可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,此类型可以表示每个信号的有效或无效状态;阻塞信号也称当前进程的信号屏蔽字,屏蔽是阻塞而不是忽略

信号集操作函数

sigset_t类型对于每种信号用一个比特位表示有效或无效,至于类型内部如何存储这些比特位则依赖于系统实现

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
int sigismember(const sigset_t *set,int signo);
  1. 函数 sigempty初始化 set所指向的信号集,使其中所有信号对应的比特位清零,表示该信号集不包括任何有效信号
  2. 函数 sigfillset初始化 set所指向的信号集,使其中所有信号对应的比特位置为1,表示该信号集的有效信号包括系统所支持的所有信号
  3. 函数 sigaddset将信号添加到信号集中;函数 sigdelset将信号从信号集中删除;函数 sigismember判断某种信号是否在信号集中

sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

如果oldset是非空指针,则读取进程当前的信号屏蔽字通过oldset参传出,如果set是非空指针,则更改进程当前的信号屏蔽字,参数how指示如何更改;如果oldsetset都是非空指针,则将原来的信号屏蔽字备份到oldset中,然后根据sethow参数更改当前的信号屏蔽字

SIG_BLOCKset包含了所有待添加到信号集中的信号
SIG_UNBLOCKset包含了所有待从信号集中删除的信号
SIG_SETMASK设置当前信号屏蔽字为set所指向的值

sigpending

int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出

默认情况下所有信号都不是阻塞的,如果信号被屏蔽,则该信号不会被递达
代码实现:屏蔽信号2

#include<iostream>
#include<signal.h>
#include<unistd.h>
#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
using namespace std;
void show_pending(const sigset_t& pending)
{
    for(int signo=MAX_SIGNUM;signo>=1;signo--)
    {
        if(sigismember(&pending,signo))
        {
            cout<<"1";
        }
        cout<<"0";
    }
    cout<<"\n";
}

int main()
{
    //1.屏蔽指定信号
    sigset_t block,oldblock,pending;
    //1.1初始化
    sigemptyset(&block);
    sigemptyset(&oldblock);
    sigemptyset(&pending);
    //1.2添加要屏蔽的信息
    sigaddset(&block,BLOCK_SIGNAL);
    //1.3开始屏蔽
    sigprocmask(SIG_SETMASK,&block,&oldblock);
    
    //2.遍历打印pending信号集
    while(true)
    {
        //2.1初始化
        sigemptyset(&pending);
        //2.2获取
        sigpending(&pending);
        //2.3打印
        show_pending(pending);
        //间隔一秒
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

捕捉信号

内核如何实现信号的捕捉

操作系统中进程存在两种状态:用户态,内核态;用户态一般会访问操作系统的资源和硬件资源,为达这一目的,必须通过系统提供的系统调用接口,而且执行系统调用的身份必须是内核,为什么用户可以访问系统调用接口呢???

在CPU中存在着许多的寄存器分为:可见寄存器,不可见寄存器,只要是和进程强相关的都是保存着进程的上下文的数据;名为CR3的寄存器保存着当前进程的运行级别:0表示内核态,3表示用户态,在系统调用接口的起始位置,存在着int 80汇编,会将用户态修改为内核态,从而可以以内核态的身份访问系统调用接口

进程以内核身份访问系统调用接口的具体过程又是怎么样的呢???
在之前进程空间中学习过,进程空间包括用户空间和内核空间,系统调用接口就与这内核空间有关:每个进程都有自己的进程空间,用户空间独享,内核空间只有一份,也就是说所有进程共享同一份内核空间;当进程访问接口时,只需要在进程空间上进行跳转即可,就类似与动态库加载到进程空间

图解:

在这里插入图片描述

当开机时,操作系统会从磁盘加载到内存中的内核区中,当进程以内核态身份访问系统调用时,会在进程空间中跳转到内核空间通过内核级页表映射到内存中操作系统完成相应的调用,完毕之后再跳转回用户空间

信号在产生时,并不会被立刻处理,而是在合适的时间被操作系统处理,这个合适的时间就是从内核态返回用户态时;所以,进程在进程切换或者系统调用时先进入内核态,在内核态中进行信号检测,也就是进程中的两个标志位(pending/block)和函数指针(handler*):如果信号未决且未被阻塞,查找函数指针是否有对应的自定义处理方法,若有,将进程内核态身份修改为用户身份完成对应的处理方法,再还原为内核身份,完成剩余的系统调用,待系统调用结束后,最后将身份修改为用户态继续执行后续的代码

图解:

在这里插入图片描述

需要注意的是:不能以内核态的身份执行用户态的代码,因为操作系统不相信任何人,以免有人进行恶意破坏

sigaction

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
  1. signum:待捕获的信号编号
  2. act:结构体指针
    在这里插入图片描述
    其中包含,处理方法sa_handler和信号集sa_mask

举个栗子,通过此函数捕获2号信号,捕获信号后休息20秒,多次向进程发送2号信号,观察进程运行结果

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

void handler(int signo)
{
    cout<<"get a signo"<<signo<<"正在处理..."<<endl;
    Count(20);
}

int main()
{
    struct sigaction act,oldact;
    act.sa_handler=handler;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,&oldact);
    while(true)
    sleep(1);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

虽然向进程发送多次同一种信号,但是进程只捕获了两次,因为当进程正在递达某一个信号时,将同种类型的信号是无法被抵达的,系统会自动将当前信号添加到屏蔽字中,将pending位图中该信号所在位置修改为0,再次发送同一信号,会将pending位图中该信号所在位置修改为1;当进程完成信号的递达时,系统会自动解除对该信号的屏蔽,所以系统会立即递达pending位图中的信号,也就是捕获第二次信号

当我们正在处理2号信号时,还想屏蔽3号信号,此时只需要将3号信号加入sa_mask信号集即可

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}

void handler(int signo)
{
    cout<<"get a signo"<<signo<<"正在处理..."<<endl;
    Count(20);
}

int main()
{
    struct sigaction act,oldact;
    act.sa_handler=handler;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    //添加3号信号
    sigaddset(&act.sa_mask,3);
    sigaction(SIGINT,&act,&oldact);
    while(true)
    sleep(1);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

和上面有所不同的是,这里的进程在最后直接退出了,其实是因为,在2号信号被接触屏蔽后,再次执行2号信号,最后执行3号信号结束进程

进程处理信号的原则是串行处理同类型的信号,不允许递归

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/25413.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分布式简要说明

1.分布式简要说明 《分布式系统原理与范型》定义&#xff1a; 分布式系统是若干独立计算机的集合&#xff0c;这些计算机对于用户来说就像单个相关系统。 分布式系统 (distributed system) 是建立在网络之上的软件系统。 随着互联网的发展&#xff0c;网站应用的规模不断扩…

SAP-QM-物料主数据-质量管理视图字段解析

过账到质检库存&#xff1a;要勾选&#xff0c;否则收货后库存不进入质检库存HU检验&#xff1a;收货到启用HU管理的库位时产生检验批&#xff0c;例如某个成品物料是收货到C002库位&#xff0c;该库位启用了HU管理&#xff0c;那么此处要勾选。但是如果勾选了&#xff0c;却收…

04.hadoop上课笔记之java编程和hbase

1.win查看服务 netstat -an #linux也有#R数学建模语言 SCALAR 2.java连接注意事项,代码要设置用户 System.setProperty("HADOOP_USER_NAME", "hadoop");3.伪分布式的好处(不用管分布式细节,直接连接一台机器…,适合用于学习) 4.官方文档 查看类(static |…

Python期末复习题库(下)——“Python”

小雅兰期末加油冲冲冲&#xff01;&#xff01;&#xff01; 1. (单选题)下列关于文件打开模式的说法,错误的是( C )。 A. r代表以只读方式打开文件 B. w代表以只写方式打开文件 C. a代表以二进制形式打开文件 D. 模式中使用时,文件可读可写 2. (单选题)下列选项中,以追加…

webpack的使用

一、什么是webpack&#xff1f; webpack是一个前端构建工具&#xff0c;目前比较主流的构建工具&#xff0c;自定义的模块比较多。 二、应用场景 vue、react、angular 都可以通过webpack构建全部可供访问的页面数量不超过500个 三、安装 通过npm方式在项目根目录下执行命令…

htmlCSS-----CSS选择器(下)

目录 前言&#xff1a; 2.高级选择器 &#xff08;1&#xff09;子代选择器 &#xff08;2&#xff09;伪类选择器 &#xff08;3&#xff09;后代选择器 &#xff08;4&#xff09;兄弟选择器 相邻兄弟选择器 通用兄弟选择器 &#xff08;5&#xff09;并集选择器 &am…

【JavaSE】Java基础语法(二十六):Collection集合

文章目录 1. 数组和集合的区别2. 集合类体系结构3. Collection 集合概述和使用【应用】4. Collection集合的遍历【应用】5. 增强for循环【应用】 1. 数组和集合的区别 相同点 都是容器,可以存储多个数据不同点 数组的长度是不可变的,集合的长度是可变的 数组可以存基本数据类型…

基于Yarn搭建Flink

基于Yarn搭建Flink 1. 概述 1.1 Yarn 简介 Apache Hadoop YARN是一个资源提供程序&#xff0c;受到许多数据处理框架的欢迎。Flink服务被提交给 YARN 的 ResourceManager&#xff0c;后者再由 YARN NodeManager 管理的机器上生成容器。Flink 将其 JobManager 和 TaskManager…

python爬虫笔记

Python爬虫笔记 一. Urllib 1. 基础请求 指定url请求返回值解码返回结果的一些操作 import urllib.request as req # 定义一个url url http://www.baidu.com# 发送请求获得相应 res req.urlopen(url)# read返回字节形式的二进制数据,需要用指定编码来解码 content res.r…

vue的虚拟DOM

vue的虚拟DOM 什么是虚拟DOM 虚拟DOM提供了一个与平台无关的抽象层&#xff0c;将应用程序的界面表示抽象为一个虚拟的DOM树。这意味着开发人员可以使用相同的代码和逻辑来描述应用程序的用户界面&#xff0c;而不需要关心具体的平台实现细节。虚拟DOM允许开发人员使用一种统…

Linux命令

准备工作 安装centos7 在镜像网下载DVD.iso或者DVD.torrent&#xff08;bit种子&#xff09;。在VMware中配置相应的信息&#xff0c;并引入iso文件&#xff0c;以便后续安装。local中&#xff1a;选择语言和时区上海software中&#xff1a;选择安装软件的内容&#xff0c;可…

基于多动作深度强化学习的柔性车间调度研究(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

通过location实现几秒后页面跳转

location对象属性 location对象属性 返回值location.href获取或者设置整个URLlocation.host返回主机&#xff08;域名&#xff09;www.baidu.comlocation.port 返回端口号&#xff0c;如果未写返回空字符串location.pathname返回路径location.search返回参数location.hash返回…

Apache网页与安全优化

一、Apache网页优化 在企业中&#xff0c;部署Apache后只采用默认的配置参数&#xff0c;会引发网站很多问题&#xff0c;换言之默认配置是针对以前较低的服务器配置的&#xff0c;以前的配置已经不适用当今互联网时代。为了适应企业需求&#xff0c;就需要考虑如何提升Apache…

遗传算法(Genetic Algorithm)

本文为阅读《遗传算法原理及应用》的笔记和心得 ISBN&#xff1a;7-118-02062-1 遗传算法简介 遗传算法是模拟生物在自然环境中的遗传和进化过程中而形成的一种自适应全局优化概率搜索算法 总的来说&#xff0c;求最优解解或近似最优解的方法主要有三种&#xff1a;枚举法、启…

数据库系统的结构

数据库模式基本概念 1.型与值 型&#xff1a;对某一类数据的结构和属性的说明。值&#xff1a;型的具体赋值。 2.模式和实例 模式&#xff1a; 数据库中全体数据的逻辑结构和特征的描述。简单来说就是数据的定义和描述。模式是元数据&#xff0c;数据是变化的&#xff0c;模…

Linux:/dev/tty、/dev/tty0 和 /dev/console 之间的区别

在Linux操作系统中&#xff0c;/dev/tty、/dev/tty0和/dev/console是三个特殊的设备文件&#xff0c;它们在终端控制和输入/输出过程中扮演着重要的角色。尽管它们看起来很相似&#xff0c;但实际上它们之间存在一些重要的区别。本文将详细介绍这三个设备文件之间的区别以及它们…

【C++系列P3】‘类与对象‘-三部曲——[基础知识](1/3)

前言 大家好吖&#xff0c;欢迎来到 YY 滴 C系列 &#xff0c;热烈欢迎&#xff01; 【 类与对象-三部曲】的大纲主要内容如下&#xff1a; 如标题所示&#xff0c;本章是【 类与对象-三部曲】三章中的第一章节——基础知识章节&#xff0c;主要内容如下&#xff1a; 目录 一.…

如何用Python写个网页爬取程序

如何用Python写个网页爬取程序 准备开发工具安装PythonPython安装pipPip安装爬取插件准备好网页地址代码实现 准备开发工具 额&#xff0c;作者用的是vscode。具体怎么安装自行百度哈&#xff0c;这个都不会建议就不要学爬取了。 不忍心藏着也&#xff0c;给你个方法吧 vsc…

计算机系统漫游

重点理解部分&#xff1a; 系统硬件&#xff1a;对硬件如处理器、存储器、I/O设备有一个基本的认识&#xff0c;理解它们的基本工作原理以及它们是如何协同工作的。Hello&#xff0c;World程序运行的过程&#xff1a;了解一个C程序如何从源代码到最终在计算机上运行的全过程。…