【Linux的进程篇章 - 进程的理解】

Linux的基本指令

前言:
前篇了解学习了Linux的进程概念、冯诺依曼体系结构,操作系统等基础知识,这篇介绍开始学习LInux的进程更深一层次的理解、进程的状态、进程的切换、僵尸和孤儿进程等相关内容,更深入地了解这个强大的开源操作系统。
/知识点汇总/

1、再次理解进程

1.1、体系结构的层次划分结构

1.用户层主要功能:指令操作、开发操作、管理操作
2.用户操作接口层主要功能: shell外壳、lib、部分指令
3.system call层主要功能:系统调用接口
4.操作系统层主要功能:内存管理、进程管理、文件管理、驱动管理
5.驱动程序层主要功能:网卡驱动、硬盘驱动、其它硬件驱动
6.底层硬件层主要功能:网卡、硬盘、其它硬件

1.2、系统调用接口的理解

用户要访问操作系统,并不是直接进行操作的,而是必须通过系统调用的方式,才能对操作系统进行操作。
理解经典例子来理解:银行工作人员和客户的关系,银行内部信息不能让外部人员随意去自行更改,需要设立一些业务窗口,安置一些专业的工作人员来对应处理客户的业务。这样的窗口,既保证的银行内部的数据安全,也完成了外部客户的需要。被称为系统调用接口。
用户操作接口常见于使用库,比如C/C++标准库…
Linux中使用ldd命令:查看对应的程序所用到了哪一个库
比如ldd processbar —> lib64/libc.so.6(C标准库)
库在系统操作接口层,为用户提供了便利。其次,提供了跨平台性。
操作系统的不同,其使用的接口不同,在不同的平台使用相同的软件/工具可能不兼容,则称为不具备跨平台性。

1.3、操作系统的管理和调用

首先为什么需要操作系统?
因为我们想让其对下层,进行软硬件管理工作,对上层提供良好的稳定的安全的运行环境。
即,本质是软件工具,所以最终以人为本的服务目的。

系统调用与库函数的关系:
在开发角度,操作系统会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用与库函数之间是存在的上下层关系,系统调用在使用上,功能比较基础,对用户的要求相对较高,所以开发者可以对部分系统调用进行适度的封装,从而形成库,有了库,就很有利于更上层或开发者进行二次开发。

windows中的进程:
windows中的任务管理器显示的就是进程。
说明了,操作系统中,进程是可以同时存在多个进程运行的。

1.4、操作系统怎么管理进程呢?

核心总结六个字:“先描述,再组织

一个文件通常在没使用或运行时,保存在磁盘中:
接着将代码或数据,加载或“拷贝”到内存中(流程回想冯诺依曼),执行/打开使用;
当把多个需要执行的程序都加载到内存中,如何确认优先级?如何调度?
此时被加载到内存的程序/文件,并不是进程,只是属于该进程所对应的代码或数据。
那么怎么对其大量被加载到内存的程序/文件进行管理呢?
本质还是“先描述再组织”。
以processbar程序为例:

#include "myproc.h"
#include <string.h>
#include <unistd.h>

#define STYLE '='
#define ARROW '>'
#define SIZE 101

void process()
{
  const char* cursor = "|/-\\";
  char buf[SIZE];
  memset(buf, 0 , SIZE);
  int i = 0;
  while(i <= 100)
  {
    printf("[%-100s][%d%%][%c]\r", buf, i, cursor[i%4]);
    fflush(stdout);
    buf[i++] = STYLE;
    if(i != 100 )buf[i] = ARROW;
    usleep(100000);
  }
  printf("\n");
}

操作系统会为每个进程构建一个结构体,即,先描述结构体对象:

struct PCB
{
    //存放进程所有属性
    //比如:
    struct PCB* next;
    //内存指针....
}

但是请注意,一个进程对应一个PCB,是一对一的关系。
然后,也就对应一个内存指针,指到对应的内存地址。
所以进程 = PCB + 代码程序和数据
片面的理解,对进程的管理就是对PCB或链表的管理,即增删查改。

1.5、什么是PCB?

PCB(process control block) — 进程控制块,可理解为用于描述进程对象的数据结构。
所以根据前面的理解,操作系统的管理本质是对PCB的管理。并不是直接管理代码和数据。

1.6、为什么要有PCB呢?

因为要管理进程就得先“描述,再组织”。
进程 = PCB + 代码程序和数据

task_struct以一个具体的控制块为例:
操作系统调度运行进程,本质就是让进程的控制块task_struct进行排队。达成间接的进程的控制。
经典例子:简历的筛选和排队 – 简历就是描述一个人的信息,对简历的增删查改就是间接对人的相应操作。
这就是进程控制块的意义。

具体的:
进程 = 内核task_struct结构体 + 代码程序和数据
其次为了更好的“描述和组织”,每一个进程都有一个自己唯一的标识符,叫做进程pid

理解一个概念,如何理解进程的动态运行?
只要我们的进程的task_struct将来在不同的队列中,进程就可以访问不同的资源。

1.7、PCB(task_struct结构体)的属性

进程task_struct结构体的属性有哪些?

1.7.1、启动

此时./process就可以称为**(执行可执行程序),即启动进程**。
(1)./xxx 本质就是让系统创建进程并运行 — 我们自己写的程序形成的可执行程序 — 系统命令 == 可执行文件/程序。
即:在linux中运行的大部分执行操作,本质都是运行进程。

Linux中查看进程的命令:

ps axj 进程名
ps – 用于查看你当前的命令当中,所对应的进程
axj – a–>all — xj -->显示对应进程的详细信息
进程名默认与启动名(可执行程序名)一样

如:ps axj | grep myprocess – 指定查找的进程

解释:我们用管道把输出的结果交给grep,然后grep按照关键字myprocess进行行过滤的查找

如:ps axj | head -1

解释:提取输出文本的前n行

&& 可同时执行两个指令(指令级联)

ls && touch log.txt 指令级联

(2).每一个进程都有一个自己唯一的标识符,叫做进程pid

(3).一个进程想要获取自己的pid,怎么拿到呢?
由操作系统提供的系统调用接口,拿取pid --> getpid
man 2 getpid
头文件

#include <sys/types.h>
#include <unistd.h>
函数原型
pid_t getpid(void)
pid_t getppid(void) – 获取当前进程的父进程
关系:父进程创建子进程
测试代码:myprocess.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t pid = getpid();
  while(1)
  {
    printf("I am a process,pid:%d\n", pid);
    sleep(1);
  }
  return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t id = getpid();
  pid_t ppid = getppid();
  while(1)
  {
    printf("I am a process,pid:%d,ppid:%d\n",id,ppid);
    sleep(1);
  }
  return 0;
}

(4)Ctrl + C用于终止程序(进程) – kill -9 标识符 – 可用于直接杀掉进程

1.7.2、进程创建的代码方式(重点fork的方式)

shell脚本的查看进程命令写法:实现每隔一定的周期执行其中的命令

while :; do ps axj | head -1 && ps axj | prep myprocess ; sleep 1;done

发现:进程每次启动的pid不一样,但父进程id一样。是正常的。

查看父进程

ps axj | head -1 && ps axj | prep 3299(父进程id)

首先,创建进程,内存中就会多一份被加载的代码和数据,进程也相应增加一个;
其次,用户无法对内核数据结构进行操作,只能通过系统提供的系统调用进行相应的操作(增删查改)。

Linux中创建子进程的(系统调用)命令fork:
man fork
头文件
:#include <unistd.h>
原型:pid_t fork(void)
返回值:fork的返回值,比较特殊能返回两次,并返回的不同返回值

-1 ---- fork创建/调用子进程失败
==0 — 返回0是判断创建子进程成功
!=0 — 返回非负整数,表示返回新创建的子进程的进程ID,即,父进程返回的子进程pid

fork语句之后的程序,父子进程共享代码,fork创建一个进程,本质是系统中多一个进程;
而多一个进程,也就是多一个进程控制块(task_struct)。

父进程的代码和数据是从磁盘加载来的;子进程的代码数据是哪里来的呢?
子进程会默认继承父进程的代码和数据。

我们为什么要创建一个子进程呢?
通常来说,是想要实现子进程执行和父进程不一样的代码。
常规的程序都是单进程,不可能同时跑不同的代码内容。
多进程情况下,则是合理的。

而对于普通的单进程程序,只能有一个返回值;对于多进程,那么为什么同一个程序同一个id,会有两个值,即是0又是!=0?
其次,fork怎么会有两个返回值,即返回两次呢?怎么做到的呢?(涉及虚拟地址空间和父子写时拷贝知识)

答:fork也是一个函数,只不过属于系统调用。
然后平常的普通程序中的return在fork执行语句之后,所以当执行完fork后,父子代码数据共享,
子进程代码中自然也有了return语句,所以父子进程各自被调度一次,那么自然就返回两次且值可以不同

引出一个概念
理论上,子进程被创建后是空的,自然其代码和数据就从父进程哪里拷贝获取,
就比如有window的任务管理器,有两个子进程由一个父进程创建,当此时关系结束一个子进程会影响其它进程?
当然不会的,逻辑上是父子等关系,实际上是分裂独立的。
所以进程一定要具备的特点就是:具备独立性,即一个进程出现问题,绝不能影响其它进程。
进程 = 内核task_struct结构体(唯一的pid) + 代码程序(只读/共享的)和数据(父子数据各自独立,原则上数据需要分开)
每一个进程都有一个自己唯一的标识符,叫做进程pid

代码实现fork一次创建多个进程。
测试代码:myprocess.c

#include <stdio.h>
#include <unistd.h>
#include <sys/file.h>

int main()
{
  printf("process is running ,only me!,pid:%d\n",getpid());
  sleep(3);
  pid_t id = fork();
  if(id == -1)
    return 1;
  else if(id == 0)
  {
    //child
    while(1)
    {
       printf("id:%d,I am child process ,pid:%d,ppid:%d\n",id,getpid(),getppid());
       sleep(1);
    }
  }
  else
  {
    //father
    while(1)
    {    
      printf("id:%d,I am a parent process,pid:%d,ppid:%d\n",id,getpid(),getppid());
      sleep(2);
    }
  }
 // printf("hello world,pid:%d,ppid:%d\n",getpid(),getppid());
  sleep(2);
  return 0;
}

小结:
1.fork()函数在Linux中用于创建一个新的进程,这个新进程是当前进程的复制品,称为子进程。
2.fork()函数的特点在于它只被调用一次,但却能返回两次:一次在父进程中,一次在子进程中。
3.关于fork()函数的返回值,具体总结如下:
a、在父进程中,fork()函数返回新创建的子进程的进程ID(PID),这个PID是一个非负整数,用于唯一标识子进程。
b、在子进程中,fork()函数返回0,这是因为在子进程的上下文中,它自己是新创建的进程,所以没有其他的进程ID可以返回。返回0表示子进程成功创建。
c、如果fork()函数执行过程中发生错误,例如由于系统资源不足而无法创建新的进程,那么它会返回一个负值。这个负值通常表示特定的错误码全局变量 errno 以指示错误原因,可以通过检查这个值来确定具体的错误原因。
4.需要注意的是,fork()函数创建的子进程是父进程的副本,包括父进程的代码、数据、堆、栈等内容都会被复制到子进程中。
但是,这两个进程有不同的进程ID,并且它们从fork()函数的返回点开始独立执行。
因此,通过检查fork()函数的返回值,可以在父进程和子进程中执行不同的代码路径,从而实现并发执行或创建多进程程序。

1.fork() 系统调用复制的是整个父进程的地址空间内容,包括父进程的代码、数据、堆、栈等内容。这意味着子进程会得到父进程代码的一个完整副本,以及父进程在调用 fork() 时的所有变量状态。
2.重要的是要理解,fork() 并不“复制”父进程之后要执行的代码;它复制的是父进程在调用 fork() 那一刹那的状态。之后父进程和子进程会分别执行自己的代码路径,但两者都是从 fork() 调用之后开始继续执行的。
3.当 fork() 返回后,父进程和子进程都会从 fork() 调用之后的下一条指令开始执行。但是,由于 fork() 的返回值不同(父进程中返回子进程的PID,子进程中返回0),所以通常会有条件语句来判断当前是父进程还是子进程,并据此执行不同的代码逻辑。
4.这里有一个关键点需要理解:在 fork() 返回之后,父进程和子进程是独立运行的,它们有自己的地址空间副本,并且可以独立地修改其变量和状态。尽管它们开始时是相同的,但随着时间的推移,它们可能会因为执行不同的代码路径和修改不同的数据而变得不同。

1.7.3、task_struct属性信息

task_struct属性信息:
1.标识符
2.状态
3.优先级
4.程序计数器
5.内存指针
6.上下文数据
7.I/O状态信息
8.记账信息
9.其他信息

Linux中一切皆文件,那么在文件中查看进程命令是什么呢?
查看进程:/proc(文件中)

ls /proc
查看当前运行的进程(同比,windows的任务管理器)
ls /proc/标识符pid -l
比如:ll /proc/pid

以get_pid.c为例子。则执行 ll /proc/20864 命令结果为:
在这里插入图片描述
可以看到结果有许多信息,其中先关注以下两点:
1.其中exe表示该进程是由那个路径下的文件加载到内存来的。
其次删除掉exe为什么再次运行,进程仍然可以跑?
那是因为你此时删除的只是属于磁盘上的exe文件,不影响已加载到内存上进程的运行。
小结:所以进程的PCB中会记录自己所对应的可执行程序的路径。

2.其中cwd表示current work dir当前工作路径,即进程的当前工作路径。

1.7.4、回顾以前的C语言文件操作

以"log.txt",文件为例:

fopen原型:FILE * fopen(const char * path, const char * mode);

对于fopen(“log.txt”,“w”)使用相对路径时,“w"是当前路径下新建。现在就来理解为什么没有"log.txt”,文件时,是当前路径下创建新文件?

FILE *fp;  
fp = fopen("example.txt", "r");  
if (fp == NULL) {  
    printf("Error opening file\n");  
    return 1;  
}  
// 在这里进行文件读取操作...  
fclose(fp);  // 记得在完成文件操作后关闭文件

那么到底什么是进程的当前路径呢?
答:每个进程在启动的时候,会记录自己当前在的哪个路径下启动,被记录的这个路径就是进程的当前路径。( 回看上面以get_pid.c为例子ll /proc/20864)
fopen(“log.txt”,“w”); – 实际上是属于进程的文件被打开执行。所以,更改进程的工作目录就能改变文件被打开的路径。

补充:当然进程的当前工作目录也是可以被修改的。

man chdir
chdir – 改变当前进程的工作目录
原型:int chdir(const char* path);

可使用chdir改变进程的工作目录,从而实现在另一个进程目录下执行文件操作了。可通过ll + proc + pid查看验证。更加说明了,更改进程的工作目录就能改变文件被打开的路径。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  //改变当前进程的工作路径
  //ll + proc + pid查看验证
  chdir("/home/project/lesson12");//由lesson13改变到lesson12,结果新文件被创建到了lesson12目录下了

  FILE* fp = fopen("log.txt","w");
  if(fp == NULL)
  {
    perror("fopen");
    _exit(1);
  }
  fclose(fp);
  while(1)
  {
    printf("I am a process,pid:%d\n",getpid());
    sleep(1);
  }
  return 0;
}

2、进程状态

2.1、Linux的进程状态

首先,任何一个进程在运行时都会有一个状态.
状态就是属于task_struct的一个属性而已 – status。
所以不要想得很复杂,因为可理解为更改状态就是更改task_struct中状态属性参数而已。

2.2、对于操作系统的内核角度状态分类

1.R运行状态(running):

进程要么正在CPU上执行,要么在等待被调度执行。这并不意味着进程一定在运行中,而是表示进程处于可被调度的状态。

2.S睡眠状态(sleeping):

进程在等待某个事件完成。这种睡眠状态有时也被称为可中断睡眠(interruptible
sleep)。当等待的事件发生时(由外部中断触发或由其他进程触发),进程将被唤醒并继续执行。

3.D磁盘休眠状态(Disk sleep):

有时也称为不可中断睡眠状态(uninterruptible
sleep)。在这种状态下,进程通常会等待I/O操作(如磁盘读写)的完成。在这个状态下,进程是不能被中断的。

4.T停止状态(stopped):

进程被停止执行。这通常是因为进程收到了一个SIGSTOP信号,导致它进入停止状态。

5.Z僵尸状态(zombie):

表示一个进程已经终止,但其父进程尚未读取其终止状态信息。僵尸进程不占用除进程表项外的任何系统资源,但若父进程不回收,它们会一直存在,这可能导致系统进程表被耗尽。

6.X死亡状态(dead):

表示进程已经终止,并且其所有资源已经被释放。这是进程生命周期的最后一个状态。

需要注意的是,进程的状态并不是静态的,而是随着进程的执行和操作系统的调度而动态变化的。同时,不同的操作系统或不同的工具可能对进程状态的命名和分类略有差异。

以简单的测试代码test_status.c为例:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
  // 运行状态
  //while(1)
  //{}
  
  // 休眠状态
  while(1)
  {
    printf("I am a process,pid:%d\n",getpid());
    sleep(5);
  }
  return 0;
}

查看代码的现象可知:

ps ajx | grep teststatus:指定进程的信息。

结合shell语句:

while :; do ps ajx | head -1 && ps ajx | grep teststatus | grep -v grep;sleep 1;done
意思是一直循环每秒反复的执行这条长语句命令。

结果表示
代码处于死循环是为R状态; – 运行状态
当加入死循环打印printf处于S状态。 – 休眠状态
其本质原因
CPU运行进程是很快的,而调用的printf是向屏幕外设交互,是需要等待硬件资源就绪的
速度显然没有CPU快的,所以CPU太快的就处于等待/休眠状态了
小结S:休眠状态:

1.本质就是等待资源就绪。
2.此时由Ctrl+C能中断的属于,可中断睡眠状态。
补充
Ctrl + C用于终止程序(进程) – kill -9 pid标识符 --》用于直接杀掉进程
kill -19 pid — 进程暂停 --T:暂停状态
kill -18 pid — 进程继续

kill -l查看kill选项

简单说明
进程的暂停状态:经典的例子就是调试程序时,打的断点。
碰到断点就是处于暂停状态(T)。
t追踪遇到断点就暂停。

D(磁盘睡眠状态)的理解
Linux系统比较特有的一种状态(针对磁盘的一种状态 – 不可中断睡眠 – 深度睡眠)。
此时进程只有自己醒来,否则只有重启或断电 – 可理解为卡死。
死亡和僵尸状态存在一个死亡等待,一直未处理(父进程结束,未对子进程作回收尸体工作)就变成了僵尸状态。
当遇见内存空间严重不足时,Linux操作系统有权利杀掉进程来释放空间。就可能会抛弃正在加载的进程,导致数据丢失。
所以为了解决这种特殊情况,就设定为D(不可被杀,不可中断的深度睡眠状态)
其次对于D状态怎么释放或者结束呢?
1.操作系统自主解决
2.重启

2.3、僵尸进程

X(死亡状态)
Z(僵尸状态):已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程来读取。如果没有父进程来读取,则僵尸进程则会一直存在(死等)。
以测试代码:zombie.c为例:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    int cnt = 5;
    while(cnt)
    {
      printf("I am a child process,cnt:%d,pid:%d\n",cnt,getpid());
      sleep(1);
      cnt--;
    }
  }
  else
  {
    //parent
    while(1)
    {
      printf("I am a parent process ,running always!,pid:%d\n",getpid());
      sleep(1);
    }
  }
  return 0;
}

前面的理解知道:进程 = 内部数据结构的task_struct + 进程的代码和数据
其中对于此时的僵尸进程,代码和数据在僵尸进程运行结束后就释放了,而task_struct的结构体会不会被释放,且一直存在不被释放。
那么将会导致,进程资源减少,内存空间减少、内存泄漏等问题。
最后一旦通过一定的方式(waitpid())僵尸进程被读取回收了,那么就会由Z状态 —> X状态,转变由操作系统进行完成相应的释放工作。
注意kill命令也无法杀掉僵尸进程。

2.4、孤儿进程

**当一个进程的父进程先退出了,那么此时这个进程就被称为孤儿进程。**由于成为孤儿进程了,就无法被父进程回收了,那么一般就由祖先进程(1号进程)领养回收掉。否则一直没有回收,则被转变为僵尸进程。
测试代码:orphan.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    while(1)
    {
      printf("I am a child process,running always!, pid:%d\n",getpid());
      sleep(1);
    }
  }
  else
  {
    //parent
    int cnt = 5;
    while(cnt)
    {
      printf("I am a parent process,cnt:%d, pid:%d\n",cnt,getpid());
      sleep(1);
      cnt--;
    }
  }
  return 0;
}

父进程如果先退出,子进程就会变成孤儿进程。但是又不能让它始终不回收,所以这种情况下,一般都会由1号进程(OS)进行领养执行回收。

为什么孤儿进程要被OS领养呢?
因为依旧要保证子进程被回收。

那么我们从前所写的程序进程,从没有关心过僵尸呢?
答:没考虑过内存泄漏。主要是因为我们通常直接在命令行中启动的进程,它的都是父进程是bash,由bash会自动回收子进程的。

2.5、阻塞与挂起和运行

1.运行态®:
都是基于CPU运行,然后由运行队列进行管理和调度。

一个进程一旦持有CPU,会一直运行到这个进程结束吗?
答:不会,是基于时间片进行轮循调度的。时间片也属于task_struct属性之一。

并发和并行:
让多个进程以切换的方式进行调度,在同一个时间段内得以同时推进代码运行,就叫做并发。
任何时刻,都同时有多个进程在真的同时运行,我们叫做并行。
注意:对于Linux的调度方式实际上并不是这样的简单队列。以上的方法,仅仅属于操作系统调度算法中的一个。

2.阻塞态:
C语言中常用到的scanf,在等待用户输入的状态属于阻塞吗?一直不输入会发生什么呢?
本质是处于等待键盘资源是否就绪(可中断状态),进而验证了,操作系统对软硬件的管理。
其次,操作系统对于硬件也是对硬件设备的数据进行管理,设备id,状态,编码…
同理,也就是形成了task_struct的设备属性的管理。如设备队列。

所以不是只有CPU才有运行队列,各种设备也有各自的等待队列(wait_queue).
管理的本质就是:先描述,再组织。

阻塞和运行的状态的变化,往往伴随这PCB被连入不同的队列中;
**入队列的不是进程的什么代码和数据而是进程的task_struct数据结构对象,**并对它进行管理,完成对应的进程/设备管理。
其次把等待队列放回运行队列的操作称为唤醒。

3.挂起态:
当操作系统在调度吃紧时,内存大量的进程都在等待阻塞中,那么此时就把代码数据暂时先换出到磁盘中,并释放内存中对应区域的空间,从而缓解内存的压力。
再直到通过唤醒,即等调度到该进程时,再加载/换入到内存中运行队列执行。
另外,此时进程是仍然存在的,换入换出的只是代码和数据部分(占据大量内存),留下的是该进程对应的task_struct依旧内核中进行管理。这种状态就称为挂起态。
利用这样的机制,很好的解决了os内存和调度吃紧的情况,提升整体执行效率。
即,磁盘和内存间的数据交换的过程,称为挂起态。

频繁的换入换出会存在什么问题吗?
频繁的换入换出会导致效率问题。相当于牺牲效率换取空间(swap分区小空间)。
所以一般适用于os吃紧时应用。

3、进程的切换问题

进程的切换会涉及CPU中的大量寄存器的工作,与task_struct交互;CPU的寄存器中会保存,1号进程/祖先进程的临时数据。(联想函数调用知识可知道,函数的返回值由寄存器带回来的)。
再联想中断的过程。保存数据上下文 – 》 恢复数据上下文。(例子:当兵,保留学籍,退伍,恢复学籍)。CPU内部的所有的寄存器中临时数据,叫做进程的上下文。

结论:进程在切换,最重要的事情就是上下文数据的保护和恢复。

CPU内部的寄存器:
寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套。
但是,CPU内部数据,可以有多套,也有多个进程,那么相对应的就有几个上下文数据。
寄存器 并不能笼统的说成代表是, 寄存器的数据内容。
即:寄存器 != 寄存器的值

最近更新

  1. leetcode705-Design HashSet

    2024-04-02 18:30:03       8 阅读
  2. Unity发布webgl之后打开streamingAssets中的html文件

    2024-04-02 18:30:03       8 阅读
  3. vue3、vue2中nextTick源码解析

    2024-04-02 18:30:03       8 阅读
  4. 高级IO——React服务器简单实现

    2024-04-02 18:30:03       8 阅读
  5. 将图片数据转换为张量(Go并发处理)

    2024-04-02 18:30:03       7 阅读
  6. go第三方库go.uber.org介绍

    2024-04-02 18:30:03       8 阅读
  7. 前后端AES对称加密 前端TS 后端Go

    2024-04-02 18:30:03       9 阅读

热门阅读

  1. 【软考高项范文】论信息系统项目的进度管理

    2024-04-02 18:30:03       6 阅读
  2. WebRTC即时通讯核心协议-TRUN(RFC5766)

    2024-04-02 18:30:03       3 阅读
  3. 队列的顺序实现

    2024-04-02 18:30:03       2 阅读
  4. 松鼠症患者福音-视频下载工具

    2024-04-02 18:30:03       3 阅读
  5. set的一些用法和问题

    2024-04-02 18:30:03       4 阅读
  6. 【Tomcat】Apache Tomcat 8.5正式结束官方支持

    2024-04-02 18:30:03       4 阅读
  7. macad.interaction解析liveactions、panels

    2024-04-02 18:30:03       3 阅读
  8. 从唯一序列码、单例模式到集群的思考

    2024-04-02 18:30:03       4 阅读
  9. promise.race方式使用

    2024-04-02 18:30:03       3 阅读
  10. abc-347

    2024-04-02 18:30:03       4 阅读
  11. Ubuntu 大压缩文件解压工具

    2024-04-02 18:30:03       4 阅读
  12. 生信小白菜之关于mutate函数的一切

    2024-04-02 18:30:03       2 阅读