操作系统对CPU的虚拟化——进程的抽象、进程相关API以及进程的受限执行

2023-10-31

因为我们计算机的cpu处理器数量是有限的,但是操作系统理论上可以同时并发的运行无数个进程,那么cpu内部是如何实现的呢?

操作系统是通过对CPU进行虚拟化来实现的。

本篇博客主要讲述操作系统对CPU的虚拟化中的进程相关的知识,包括进程的抽象,进程的相关API,进程的执行。

不要一说到虚拟化就感觉非常高大上,此处的虚拟化是指:虽然我们的处理器是有限的,一个cpu不能同时分配给多个进程使用,但是操作系统可以对CPU进行虚拟化,让每个进程都认为自己占有了一个cpu。并且对cpu的虚拟化使用到了时分复用的思想。

本系列博客是我在看《操作系统导论》中的3-6章之后的一些总结,和书中的一些内容,我觉得这本书讲的通俗易懂,比较适合基础不扎实的小伙伴学习,小伙伴们在看本篇博客的时候可以查找自己感兴趣的内容跳跃阅读。

抽象:进程

本章讨论操作系统提供的基本的抽象——进程。进程的非正式定义非常简单:进程就是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令(也可能 是一些静态数据)。是操作系统让这些字节运行起来,让程序发挥作用。

事实表明,人们常常希望同时运行多个程序。比如:在使用计算机或者笔记本的时候, 我们会同时运行浏览器、邮件、游戏、音乐播放器,等等。实际上,一个正常的系统可能会有上百个进程同时在运行。如果能实现这样的系统,人们就不需要考虑这个时候哪一个CPU是可用的,使用起来非常简单。

因此我们的挑战是:

如何提供有许多CPU的假象?虽然只有少量的物理CPU可用,但是操作系统如何提供几乎有无数个CPU可用的假象?

操作系统通过虚拟化(virtualizing) CPU来提供这种假象。通过让一个进程只运行一个时间片,然后切换到其他进程,操作系统提供了存在多个虚拟CPU的假象。这就是时分共享(time sharing) CPU技术,允许用户如愿运行多个并发进程。潜在的开销就是性能损失, 因为如果CPU必须共享,每个进程的运行就会慢一点。

要实现CPU的虚拟化,要实现得好,操作系统就需要一些低级机制以及一些高级智能。 我们将低级机制称为机制(mechanism)。机制是一些低级方法或协议,实现了所需的功能。 例如,我们稍后将学习如何实现上下文切换(context switch),它让操作系统能够停止运行 一个程序,并开始在给定的CPU上运行另一个程序。所有现代操作系统都采用了这种分时机制。

提示:使用时分共享(和空分共享)
时分共享(time sharing)是操作系统共享资源所使用的最基本的技术之一。通过允许资源由一个实 体使用一小段时间,然后由另一个实体使用一小段时间,如此下去,所谓的资源(例如,CPU或网络链 接)可以被许多人共享。时分共享的自然对应技术是空分共享,资源在空间上被划分给希望使用它的人。 例如,磁盘空间自然是一个空分共享资源,因为一旦将块分配给文件,在用户删除文件之前,不可能将 它分配给其他文件。

在这些机制之上,操作系统中有一些智能以策略(policy)的形式存在。策略是在操作系统内做出某种决定的算法。例如,给定一组可能的程序要在CPU上运行,操作系统应该运行哪个程序?操作系统中的调度策略(scheduling policy)会做出这样的决定,可能利用历史信息(例如,哪个程序在最后一分钟运行得更多?)、工作负载知识(例如,运行什么 类型的程序?)以及性能指标(例如,系统是否针对交互式性能或吞吐量进行优化?)来 做出决定。

抽象:进程

操作系统为正在运行的程序提供的抽象,就是所谓的进程(process)。正如我们上面所说的,一个进程只是一个正在运行的程序。在任何时刻,我们都可以清点它在执行过程中访问或影响的系统的不同部分,从而概括一个进程。

为了理解构成进程的是什么,我们必须理解它的机器状态(machine state):程序在运行时可以读取或更新的内容。在任何时刻,机器的哪些部分对执行该程序很重要?

进程的机器状态有一个明显组成部分,就是它的内存。指令存在内存中,正在运行的程序读取和写入的数据也在内存中,因此进程可以访问的内存(称为地址空间,address space) 是该进程的一部分。

进程的机器状态的另一部分是寄存器。许多指令明确地读取或更新寄存器,因此显然, 它们对于执行该进程很重要。

请注意,有一些非常特殊的寄存器构成了该机器状态的一部分。例如,程序计数器 (Program Counter, PC)(有时称为指令指针‘Instruction Pointer或IP)告诉我们程序当前 正在执行哪个指令;类似地,栈指针(stackpointer)和相关的帧指针(ftamepointer)用于管理函数参数栈、局部变量和返回地址。

最后,程序也经常访问持久存储设备。此类I/O信息可能包含当前打开的文件列表。

进程创建

我们应该揭开一个谜,就是程序如何转化为进程。具体来说,操作系统如何启动并运行一个程序?进程创建实际如何进行?

操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载(load)到内存中,加载到进程的地址空间中。程序最初以某种可执行格式驻留在磁盘上(disk,或者在某些现代系统中,在基于闪存的SSD上)。因此,将程序和静态数据加载到内存中的过程,需要操作系统从磁盘读取这些字节,并将它们放在内存中的某处。
在这里插入图片描述

在早期的(或简单的)操作系统中,加载过程尽早(eagerly)完成,即在运行程序之前全部完成。现代操作系统惰性(lazily)执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。要真正理解代码和数据的惰性加载是如何工作的,必须更多地 了解分页和交换的机制,这是我们将来讨论内存虚拟化时要涉及的主题。现在,只要记住在运行任何程序之前,操作系统显然必须做一些工作,才能将重要的程序字节从磁盘读入内存。

将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要执行其他一些操作。必须为程序的运行时栈(rumtime stack或stack)分配一些内存。你可能己经知道,很多语言编写的程序(如C或者Java)使用栈存放局部变量、函数参数和返回地址。操作系统分配这些内存,并提供给进程。

操作系统也可能为程序的堆(heap)分配一些内存。在C程序中,堆用于显式请求的动态分配数据。程序通过调用malloc()来请求这样的空间,并通过调用free()来明确地释放它。数据结构(如链表、散列表、树和其他有趣的数据结构)需要堆,起初堆会很小。随着程序运行,通过malloc()库API请求更多内存,操作系统可能会参与分配更多内存给进程, 以满足这些调用。

操作系统还将执行一些其他初始化任务,特别是与输入/输出(I/O)相关的任务。例如, 在UNIX系统中,默认情况下每个进程都有3个打开的文件描述符(file descriptor),用于标准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。

通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与I/O设置相关的其他工作,OS现在(终于)为程序执行搭好了舞台。然后它有最后一项任务:启动程序, 在入口处运行,即main()。通过跳转到main()例程,OS将CPU 的控制权转移到新创建的进程中,从而程序开始执行。

进程状态

既然已经了解了进程是什么(但我们会继续改进这个概念),以及(大致)它是如何创建的,让我们来谈谈进程在给定时间可能处于的不同状态(state)。在早期的计算机系统 [DV66, V+65]中,出现了一个进程可能处于这些状态之一的概念。

简而言之,进程可以处于以下3种状态之一。
运行(niiming):在运行状态下,进程正在处理器上运行。这意味着它正在执行指令。
就绪(ready):在就绪状态下,进程已准备好运行,但由于某种原因,操作系统选择不在此时运行该进程。
阻塞(blocked):在阻塞状态下,一个进程执行了某种操作,直到发生其他事件时才会准备运行。一个常见的例子是,当进程向磁盘发起I/O请求时,它会被阻塞,因此其他进程可以使用处理器。

如果将这些状态映射到一个图上,会得到下图:
在这里插入图片描述
可以根据操作系统的载量,让进程在就绪状态和运行状态之间转换。从就绪到运行意味着该进程已经被调度(scheduled),从运行转移到就绪意味着该进程已经取消调度(descheduled)。一旦进程被阻塞(例如,通 过发起I/O操作),OS将保持进程的这种状态,直到发生某种事件(例如,I/O完成)。此时,进程再次转入就绪状态(也可能立即再次运行,取决于操作系统的调度)。

我们来看一个例子,看两个进程如何通过这些状态转换。首先,想象两个正在运行的进程,每个进程只使用CPU(它们没有I/O)。在这种情况下,每个进程的状态可能如下表所示。
表4.4	跟踪进程状态:只看CPU
在下一个例子中,第一个进程在运行一段时间后发起I/O请求。此时,该进程被阻塞, 让另一个进程有机会运行。表4.2展示了这种场景。
在这里插入图片描述
更具体地说,Process0发起I/O并被阻塞,等待I/O完成。例如,当从磁盘读取数据或等待网络数据包时,进程会被阻塞。OS发现Process0不使用CPU并开始运行Process1。当 Process1运行时,I/O完成,将Process0移回就绪状态。最后,Process 1结束,Process0运行,然后完成。

请注意,即使在这个简单的例子中,操作系统也必须做出许多决定。首先,系统必须决定在Process0发出I/O时运行Process1。这样做可以通过保持CPU繁忙来提高资源利用率。其次,当I/O完成时,系统决定不切换回Process0,目前还不清楚这是不是一个很好的决定。你怎么看?这些类型的决策由操作系统调度程序完成,这是我们后面即将讨论的主题。

数据结构

操作系统是一个程序,和其他程序一样,它有一些关键的数据结构来跟踪各种相关的信息。例如,为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种进程列表(process list),以及跟踪当前正在运行的进程的一些附加信息,操作系统还必须以某种方式跟踪被阻塞的进程。当I/O事件完成时,操作系统应确保唤醒正确的进程,让它准备好再次运行。

下边代码(xv6的proc结构)展示了OS需要跟踪xv6内核中每个进程的信息类型。“真正的”操作系统中存在类似的进程结构,如Linux、macOS X或Windows。查看它们,看看有多复杂。

// the registers xv6 will save and restore
// to stop and subsequently restart a process
struet context {
	int eip;
	int esp;
	int ebx;
	int ecx;
	int edx;
	int esi;
	int edi;
	int ebp;
}

// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
				RUNNABLE, RUNNING, ZOMBIE };
				
// the information xv6 tracks about each process
//including its register context and state
struct proc{
	char *mem; 	//start of process memory
	uint sz; 	//size of process memory
	char *kstack;//Bottom of kernel stack for this process
	enum proc_state state;//Process state
	int pid;	//Process ID
	struct proc *parent;	//Parent process
	void *chan;	//If non-zero, sleeping on chan
	int killed;	//If non-zero, have been killed
	struct file *ofile[NOEILE]; //open files
	struct inode *cmd;	//Current directory
	struct context context; //Switch here to run process
	struct trapframe *tf;	//Trap frame for the current interrupt
}

从上边代码可以看到,操作系统追踪进程的一些重要信息。对于暂时停止的进程,寄存器上下文将保存其寄存器的内容。当一个进程停止时,它的寄存器将被保存到这个内存位置。 通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程。我们将在后面的章节中更多地了解这种技术,它被称为上下文切换(contextswitch)

从上边代码中还可以看到,除了运行、就绪和阻塞之外,还有其他一些进程可以处于的状态。有时候系统会有一个初始(initial)状态,表示进程在创建时处于的状态。另外,一个进程可以处于已退出但尚未清理的最终(final)状态(在基于UNIX的系统中,这称为僵尸状态)。这个最终状态非常有用,因为它允许其他进程(通常是创建进程的父进程)检查进程的返回代码,并查看刚刚完成的进程是否成功执行(通常,在基于UNIX的系统中,程序成功完成任务时返回零,否则返回非零)。完成后,父进程将进行最后一次调用(例如, wait()),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关 数据结构。

数据结构一进程列表
操作系统充满了我们将在这些讲义中讨论的各种重要数据结构(data structure )。进程列表(process list)是第一个这样的结构。这是比较简单的一种,但是,任何能够同时运行多个程序的操作系统当然都会有类似这种结构的东西,以便跟踪系统中正在运行的所有程序。有时候人们会将存储关于进程的信息的个体结构称为进程控制块(Process Control Block, PCB),这是谈论包含每个进程信息的C结构的一种方式。

小结

我们已经介绍了操作系统的最基本抽象:进程。它很简单地被视为一个正在运行的程序。有了这个概念,接下来将继续讨论具体细节:实现进程所需的低级机制和以智能方式调度这些进程所需的高级策略。结合机制和策略,我们将加深对操作系统如何虚拟化CPU的理解。

进程的API

先介绍一下操作系统的接口必须包含的内容,所有现代操作系统都以某种形式提供这些API:

  • 创建(create):操作系统必须包含一些创建新进程的方法。在shell中键入命令或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。
  • 销毁(destroy):由于存在创建进程的接口,因此系统还提供了一个强制销毁进程的接口。当然,很多进程会在运行完成后自行退出。但是,如果它们不退出, 用户可能希望终止它们,因此停止失控进程的接口非常有用。
  • 等待(wait):有时等待进程停止运行是有用的,因此经常提供某种等待接口。
  • 其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。例如,大多数操作系统提供某种方法来暂停进程, 然后恢复运行。
  • 状态(statu):通常也有一些接口可以获得有关进程的状态信息,例如运行了多长时间,或者处于什么状态。

本部分将讨论UNIX系统中的进程创建。UNIX系统采用了一种非常有趣的创建新进程的方式,即通过一对系统调用:fork()和exec()。进程还可以通过第三个系统调用wait(),来等待其创建的子进程执行完成。本部分将详细介绍这些接口,并举一些简单的例子。

fork()系统调用

系统调用fork()用于创建新进程。具体来说你可以运行一个程序,代码如下所示:

include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]){
	printf("hello world (pid:%d)\n"(int)getpid());
	int rc = fork();
	if(rc < 0{	//fork failed; exit
		fprintf(stderr, "fork failed'n" );
		exit(1);
	}else if (rc == 0){ //child(new process)
		printf("hello,I am child (pid:%d)\n"(int) getpid() ;
	}else {		//parent goes down this path (main)
		printf( "hello, I am parent of %d (pid:%d) \n",rc,(int) getpid());
	}
	return 0;
}

运行上述程序(p1.c),能得到如下输出:

prompt> ./p1
hello world (pid:29146)
hello, I am parent of 29147(pid:29146)
hello,工 am child (pid:29147)
prompt>

让我们更详细地理解一下p1.c到底发生了什么。当它刚开始运行时,进程输出一条hello world信息,以及自己的进程描述符(process identifier, PID)。该进程的PID是29146。在UNIX 系统中,如果要操作某个进程(如终止进程),就要通过PID来指明。到目前为止,一切正常。

紧接着有趣的事情发生了。进程调用了 fbrk()系统调用,这是操作系统提供的创建新进程的方法。新创建的进程几乎与调用进程完全一样,对操作系统来说,这时看起来有两个完全一样的p1程序在运行,并都从fork()系统调用函数中返回。新创建的进程称为子进程(child),原来的进程称为父进程(parent)。子进程不会从main()函数开始执行(因此hello world信息只输出了一次),而是直接从fork()系统调用返回,就好像是它自己调用了 fork()。

你可能已经注意到,子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从fork()返回的值是不同的。父进程获得的返回值是新创建子进程的PID,而子进程获得的返回值是0。这个差别非常重要,因为这样就很容易编写代码处理两种不同的情况(像上面那样)。

你可能还会注意到,它的输出不是确定的(deterministic)。子进程被创建后,我们就需要关心系统中的两个活动进程了:子进程和父进程。假设我们在单个CPU的系统上运行(简单起见),那么子进程或父进程在此时都有可能运行。在上面的例子中,父进程先运行并输 出信息。在其他情况下,子进程可能先运行。

CPU调度程序(scheduler)决定了某个时刻哪个进程被执行,我们稍后将详细介绍这部分内容。由于CPU调度程序非常复杂,所以我们不能假设哪个进程会先运行。事实表明,这种不确定性(non-determinism)会导致一些很有趣的问题,特别是在多线程程序(multi-threaded program)中。

wait()系统调用

到目前为止,我们没有做太多事情:只是创建了一个子进程,打印了一些信息并退出。 事实表明,有时候父进程需要等待子进程执行完毕,这很有用。这项任务由wait()系统调用(或者更完整的兄弟接口 waitpid())来实现。下面代码展示了更多细节:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys / wait.h>

int main(int argc, char *argv[]){
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if (rc < 0){	//fork failed; exit
		fprintf(stderr, "fork failed\n" );
		exit(1);
	}else if (rc == 0){ 	// child (new process)
		printf( "hello,I am child (pid:%d)\n"(int) getpid() );
	}else{	// parent goes down this path (main)
		int wc = wait(NULL);
		printf("hello, I am parent of %d(wc:%d)(pid:%d)\n",rc, wc, (int)getpid());
	}
	return O;
)

在p2.c的例子中,父进程调用wait(),延迟自己的执行,直到子进程执行完毕。当子进程结束时,wait()才返回父进程。
上面的代码增加了 wait()调用,因此输出结果也变得确定了。这是为什么呢?想想看。

下面是输出结果:

prompt> ./p2
hello world (pid:29266)
hello,I am child (pid:29267)
hello,I am parent of 29267 (wc:29267)(pid:29266)
prompt>

exec()系统调用

最后是exec()系统调用,它也是创建进程API的一个重要部分。这个系统调用可以让子进程执行与父进程不同的程序。例如,在p2.c中调用fork(),这只是在你想运行相同程序的拷贝时有用。但是,我们常常想运行不同的程序,exec()正好做这样的事。如下代码所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv[]){
	printf ("hello world (pid: %d)'n"(int) getpid());
	int rc = fork();
	if (rc < 0){	// fork failed; exit
		fprintf(stderr, "fork failed\n");
		exit(1);
	}else if (rc == o) { // child (new process)
		printf( "hello, I am child (pid:%d)\n"(int) getpid());
		char *myargs[3];
		myargs[0] = strdup( "wc" );		// program: "wc"(word count)
		myargs[1] = strdup("p3.c"); 	// argument: file to count
		myargs[2] = NULL;		// marks end of array
		execvp(myargs[0],myargs); 	// runs word count
		printf("this shouldn't print out" );
	} else {	// parent goes down this path (main)
		int wc = wait(NULL);
		printf("hello, I am parent of %d (wc: %d)(pid: %d) in",rc,wc,(int) getpid());
	}
	return 0;
}

上述程序的输出结果是:

prompt> ./p3
hello world (pid:29383)
hello, I am child (pid:29384)
	29	107	1030 p3.c
hello, I am parent of 29384(wc:29384)(pid:29383)
prompt>

在这个例子中,子进程调用execvp()来运行字符计数程序wc。实际上,它针对源代码文件p3.c运行wc,从而告诉我们该文件有多少行、多少单词,以及多少字节。

fork()系统调用很奇怪,它的伙伴exec()也不一般。给定可执行程序的名称(如wc)及需要的参数(如p3.c)后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给该进程。因此,它并没有创建新进程,而是直接将当前运行的程序(以前的p3)替换为不同的运行程序(wc)。子进程执行exec()之后,几乎就像p3.c从未运行过一样。对exec()的成功调用永远不会返回

为什么这样设计API

当然,你的心中可能有一个大大的问号:为什么设计如此奇怪的接口,来完成简单的、创建新进程的任务?好吧,事实证明,这种分离fork()及exec()的做法在构建UNIX shell的时候非常有用,因为这给了 shell在fork之后exec之前运行代码的机会,这些代码可以在运 行新程序前改变环境,从而让一系列有趣的功能很容易实现。

shell也是一个用户程序,它首先显示一个提示符(prompt),然后等待用户输入。你可以向它输入一个命令(一个可执行程序的名称及需要的参数),大多数情况下,shell可以在文件系统中找到这个可执行程序,调用fork()创建新进程,并调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令完成。子进程执行结束后,shell从wait()返回并再次输出一个提示符,等待用户输入下一条命令

fork()和exec()的分离,让shell可以方便地实现很多有用的功能。比如:

prompt> wc p3.c > newfile.txt

在上面的例子中,wc的输出结果被重定向(redirect)到文件newfile.txt中(通过newfile.txt 之前的大于号来指明重定向)。shell实现结果重定向的方式也很简单,当完成子进程的创建后,shell在调用exec()之前先关闭了标准输出(standard output),打开了文件newfile.txt。 这样,即将运行的程序wc的输出结果就被发送到该文件,而不是打印在屏幕上。

下面代码p4.c展示了这样做的一个程序。重定向的工作原理,是基于对操作系统管理文件描述符方式的假设。具体来说,UNIX系统从0开始寻找可以使用的文件描述符。在这个例子中,STDOUT_FILENO 将成为第一个可用的文件描述符,因此在open()被调用时,得到赋值。然后子进程向标准输出文件描述符的写入(例如通过printf()这样的函数),都会被透明地转向新打开的文件,而不是屏幕。

#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[]){
	int rc = fork();
	if(rc < 0{	// fork failed; exit
		fprintf(stderr, "fork failed'n");
		exit(1);
	) else if (rc == 0) { //child: redirect standard output to a file
		close(STDOUT_FILENO);
		open( "./p4.output",O_CREAT|O_WRONLY|O_TRUNC,S_IRWXU);
		
		// now exec "wc" ...
		char *myargs[3];
		myargs[0] = strdup("wc");	// program: "wc" (word count)
		myargs[l] = strdup("p4.c");	// argument: file to count
		myargs[2]= NULL;	// marks end of array
		execvp(myargs[0],myargs);	//	runs word count
	}else {	// parent goes down this path (main)
			int wc = wait(NULL);
	}
	return o;
}

下面是运行p4.c的结果:

prompt> ./p4
prompt> cat p4.output
32	109	846 p4.c
prompt>

关于这个输出,你(至少)会注意到两个有趣的地方。首先,当运行p4程序后,好像什么也没有发生。shell只是打印了命令提示符,等待用户的下一个命令。但事实并非如此, p4确实调用了fork来创建新的子进程,之后调用execvp()来执行wc。屏幕上没有看到输出, 是由于结果被重定向到文件p4.output。其次,当用cat命令打印输出文件时,能看到运行 wc的所有预期输出。

UNIX管道也是用类似的方式实现的,但用的是pipe()系统调用。在这种情况下,一个进程的输岀被链接到了一个内核管道(pipe)上(队列),另一个进程的输入也被连接到了同一个管道上。因此,前一个进程的输岀无缝地作为后一个进程的输入,许多命令可以用这种方式串联在一起,共同完成某项任务。比如通过将grep、wc命令用管道连接可以完成从一个文件中查找某个词,并统计其出现次数的功能:

grep -o foo file | wc -lo

最后,我们刚才只是从较高的层面上简单介绍了进程API,关于这些系统调用的细节,还有更多需要学习和理解。

其他API

除了上面提到的fork()、exec()和wait()之外,在UNIX中还有其他许多与进程交互的方式。比如可以通过kill()系统调用向进程发送信号(signal),包括要求进程睡眠、终止或其他有用的指令。实际上,整个信号子系统提供了一套丰富的向进程传递外部事件的途径,包括接受和执行这些信号。

此外还有许多非常有用的命令行工具。比如通过ps命令来查看当前在运行的进程,阅读man手册来了解ps命令所接受的参数。工具top也很有用,它展示当前系统中进程消耗CPU或其他资源的情况。有趣的是,你常常会发现top命令自己就是最占用资源的,它或许有一点自大狂。此外还有许多CPU检测工具,让你方便快速地了解系统负载。

小结

本部分介绍了在UNIX系统中创建进程需要的API:fork()、exec()和wait()。更多的细节可以阅读Stevens和Rago的著作,尤其是关于进程控制、进程关系及信号的章节。 其中的智慧让人受益良多。

机制:受限直接执行

为了虚拟化CPU,操作系统需要以某种方式让许多任务共享物理CPU,让它们看起来像是同时运行。基本思想很简单:运行一个进程一段时间,然后运行另一个进程,如此轮换。通过以这种方式时分共享(time sharing) CPU,就实现了虚拟化。

然而,在构建这样的虚拟化机制时存在一些挑战。第一个是性能:如何在不增加系统开销的情况下实现虚拟化?第二个是控制权:如何有效地运行进程,同时保留对CPU的控制?控制权对于操作系统尤为重要,因为操作系统负责资源管理。如果没有控制权,一个进程可以简单地无限制运行并接管机器,或访问没有权限的信息。因此,在保持控制权的同时获得高性能,这是构建操作系统的主要挑战之一。

基本技巧:受限直接执行

为了使程序尽可能快地运行,操作系统开发人员想出了一种技术——我们称之为受限的直接执行(limited direct execution)。这个概念的"直接执行”部分很简单:只需直接在CPU 上运行程序即可。因此,当OS希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点(main()函数或类似的),跳转到那里,并开始运行用户的代码。下表展示了这种基本的直接执行协议(没有任何限制),使用正常的调用并返回跳转到程序的main(),并在稍后回到内核。
在这里插入图片描述
听起来很简单,不是吗?但是,这种方法在我们的虚拟化CPU时产生了一些问题。第一个问题很简单:如果我们只运行一个程序,操作系统怎么能确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?第二个问题:当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化CPU所需的时分共享

下面在回答这些问题时,我们将更好地了解虚拟化CPU需要什么。在开发这些技术时, 我们还会看到标题中的“受限”部分来自哪里。如果对运行程序没有限制,操作系统将无 法控制任何事情,因此会成为“仅仅是一个库”一对于有抱负的操作系统而言,这真是非常令人悲伤的事!

问题1:受限制的操作

直接执行的明显优势是快速。该程序直接在硬件CPU上运行,因此执行速度与预期的 一样快。但是,在CPU上运行会带来一个问题——如果进程希望执行某种受限操作(如向磁盘发出I/O请求或获得更多系统资源(如CPU或内存)),该怎么办?

对于I/O和其他相关操作,一种方法就是让所有进程做所有它想做的事情。但是,这样做导致无法构建许多我们想要的系统。例如,如果我们希望构建一个在授予文件访问权限前检查权限的文件系统,就不能简单地让任何用户进程向磁盘发出I/0o如果这样做,一个进程就可以读取或写入整个磁盘,这样所有的保护都会失效。

因此,我们采用的方法是引入一种新的处理器模式,称为用户模式(usermode)。在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出I/O请求。这样做会导致处理器引发异常,操作系统可能会终止进程。

与用户模式不同的内核模式(kernel mode),操作系统(或内核)就以这种模式运行。 在此模式下,运行的代码可以做它喜欢的事,包括特权操作,如发出I/O请求和执行所有类 型的受限指令。

但是,我们仍然面临着一个挑战——如果用户希望执行某种特权操作(如从磁盘读取), 应该怎么做?为了实现这一点,几乎所有的现代硬件都提供了用户程序执行系统调用的能力。系统调用允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。大多数操作系统提供几百个调用(详见POSIX标准[P10])。早期的UNIX系统公开了更简洁的子集,大约20个调用。

要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许), 从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回 (retum-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。

执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够正确返回。例如,在x86上,处理器会将程序计数器、标志和其他一些寄存器推送到每个进程的内核栈(kernel stack)上。从返回陷阱将从栈弹出这些值,并恢复执行用户模式程序。其他硬件系统使用不同的约定,但基本概念在各个平台上是相似的。

还有一个重要的细节没讨论:陷阱如何知道在OS内运行哪些代码?显然,发起调用的过程不能指定要跳转到的地址(就像你在进行过程调用时一样),这样做让程序可以跳转到内核中的任意位置,这显然是一个糟糕的主意。

内核通过在启动时设置陷阱表(trap table)来实现当机器启动时,它在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器, 并且硬件知道在发生系统调用和其他异常事件时要做什么(即跳转到哪段代码)。

最后再插一句:能够执行指令来告诉硬件陷阱表的位置是一个非常强大的功能。因此, 你可能已经猜到,这也是一项特权(privileged)操作。如果你试图在用户模式下执行这个指令,硬件不会允许,你可能会猜到会发生什么(提示:再见,违规程序)。

时间线(随着时间的推移向下,如下表所示)总结了该协议,我们假设每个进程都有一个内核栈,在进入内核和离开内核时,寄存器(包括通用寄存器和程序计数器)分别被保存和恢复。
在这里插入图片描述
LDE协议有两个阶段。第一个阶段(在系统引导时),内核初始化陷阱表,并且CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作(所有特权指令均以粗体突出显示)。第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将CPU切换到用户模式并开始运行该进程。 当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从main()返回。这通常会返回到一些存根代码,它将正确退出该程序(例如,通过调用exitO系统调用,这将陷入OS中)。此时,OS清理干净,任务完成了。

问题2:在进程之间切换

直接执行的下一个问题是实现进程之间的切换。在进程之间切换应该很简单,对吧?

操作系统应该决定停止一个进程并开始另一个进程。有什么大不了的?但实际上这有点棘手,特别是,如果一个进程在CPU上运行,这就意味着操作系统没有运行。如果操作系统没有运行,它怎么能做事情?如果操作系统没有在CPU上运行,那么操作系统显然没有办法釆取行动。因此, 我们遇到了关键问题。

协作方式:等待系统调用

过去某些系统采用的一种方式(例如,早期版本的Macintosh操作系统[M11]或旧的 Xerox Alto系统[A79])称为协作(cooperative)方式。在这种风格下,操作系统相信系统的进程会合理运行。运行时间过长的进程被假定会定期放弃CPU,以便操作系统可以决定运行其他任务。

因此,你可能会问,在这个虚拟的世界中,一个友好的进程如何放弃CPU?事实证明,大多数进程通过进行系统调用,将CPU的控制权转移给操作系统,例如打开文件并随后读取文件,或者向另一台机器发送消息或创建新进程。像这样的系统通常包括一个显式的yield系统调用,它什么都不干,只是将控制权交给操作系统,以便系统可以运行其他进程。

提示:处理应用程序的不当行为
操作系统通常必须处理不当行为,这些程序通过设计(恶意)或不小心(错误),尝试做某些不应该做的事情。在现代系统中,操作系统试图处理这种不当行为的方式是简单地终止犯罪者。一击出局! 也许有点残酷,但如果你尝试非法访问内存或执行非法指令,操作系统还应该做些什么?

如果应用程序执行了某些非法操作,也会将控制转移给操作系统。例如,如果应用程序以0为除数,或者尝试访问应该无法访问的内存,就会陷入(trap)操作系统。操作系统将再次控制CPU (并可能终止违规进程)。

因此,在协作调度系统中,OS通过等待系统调用,或某种非法操作发生,从而重新获得CPU的控制权。你也许会想:这种被动方式不是不太理想吗?例如,如果某个进程(无论是恶意的还是充满缺陷的)进入无限循环,并且从不进行系统调用,会发生什么情况? 那时操作系统能做什么?

非协作方式:操作系统控制

事实证明,没有硬件的额外帮助,如果进程拒绝进行系统调用(也不出错),从而将控制权交还给操作系统,那么操作系统无法做任何事情。事实上,在协作方式中,当进程陷入无限循环时,唯一的办法就是使用古老的解决方案来解决计算机系统中的所有问题一重 新启动计算机。因此,我们又遇到了请求获得CPU控制权的一个子问题。

答案很简单,许多年前构建计算机系统的许多人都发现了:时钟中断(timerinterrupt)。时钟设备可以编程为每隔几毫秒产生一次中断,产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupthandler)会运行。此时,操作系统重新获得CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。

首先,正如我们之前讨论过的系统调用一样,操作系统必须通知硬件哪些代码在发生时钟中断时运行。因此,在启动时,操作系统就是这样做的。其次,在启动过程中,操作系统也必须启动时钟,这当然是一项特权操作。一旦时钟开始运行,操作系统就感到安全了,因为控制权最终会归还给它,因此操作系统可以自由运行用户程序。时钟也可以关闭 (也是特权操作),稍后更详细地理解并发时,我们会讨论

请注意,硬件在发生中断时有一定的责任,尤其是在中断发生时,要为正在运行的程序保存足够的状态,以便随后从陷阱返回指令能够正确恢复正在运行的程序。这一组操作与硬件在显式系统调用陷入内核时的行为非常相似,其中各种寄存器因此被保存(进入内 核栈),因此从陷阱返回指令可以容易地恢复。

保存和恢复上下文

既然操作系统已经重新获得了控制权,无论是通过系统调用协作,还是通过时钟中断强制停止执行,都必须决定:是继续运行当前正在运行的进程,还是切换到另一个进程。这个决定是由调度程序(scheduler)做出的,它是操作系统的一部分。我们将在接下来的几部分中详细讨论调度策略。

如果决定进行切换,OS就会执行一些底层代码,即所谓的上下文切换(context switch)。 上下文切换在概念上很简单:操作系统要做的就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。

为了保存当前正在运行的进程的上下文,操作系统会执行一些底层汇编代码,来保存通用寄存器、程序计数器,以及当前正在运行的进程的内核栈指针,然后恢复寄存器、程序计数器,并切换内核栈,供即将运行的进程使用。通过切换栈,内核在进入切换代码调用时,是一个进程(被中断的进程)的上下文,在返回时,是另一进程(即将执行的进程) 的上下文。当操作系统最终执行从陷阱返回指令时,即将执行的进程变成了当前运行的进程,至此上下文切换完成。

表6.3展示了整个过程的时间线。在这个例子中,进程A正在运行,然后被中断时钟中断。硬件保存它的寄存器(在内核栈中),并进入内核(切换到内核模式)。在时钟中断处理程序中,操作系统决定从正在运行的进程A切换到进程B。此时,它调用switch()例程, 该例程仔细保存当前寄存器的值(保存到A的进程结构),恢复寄存器进程B (从它的进程结构),然后切换上下文(switch context),具体来说是通过改变栈指针来使用B的内核栈(而不是A的)。最后,操作系统从陷阱返回,恢复B的寄存器并开始运行它。
在这里插入图片描述

担心并发吗

作为细心周到的读者,你们中的一些人现在可能会想:“呃……在系统调用期间发生时钟中断时会发生什么? ”或“处理一个中断时发生另一个中断,会发生什么?这不会让内核难以处理吗?

答案是肯定的,如果在中断或陷阱处理过程中发生另一个中断,那么操作系统确实需要关心发生了什么。之后的博客我们将详细讨论。

补充:上下文切换要多长时间
你可能有一个很自然的问题:上下文切换需要多长时间?甚至系统调用要多长时间?如果感到好奇,有一种称为lmbench [MS96]的工具,可以准确衡量这些事情,并提供其他一些可能相关的性能指标。

随着时间的推移,结果有了很大的提高,大致跟上了处理器的性能提高。例如,1996年在200-MHz P6 CPU上运行Linux 1.3.37,系统调用花费了大约4微秒,上下文切换时间大约为6微秒。现代系统的性能 几乎可以提高一个数量级,在具有2 GHz 或 3 GHz处理器的系统上的性能可以达到亚微秒级。

应该注意的是,并非所有的操作系统操作都会跟踪CPU的性能。正如Ousterhout所说的,许多操作系统操作都是内存密集型的,而随着时间的推移,内存带宽并没有像处理器速度那样显著提高。 因此,根据你的工作负载,购买最新、性能好的处理器可能不会像你希望的那样加速操作系统。

为了让你开开胃,我们只是简单介绍了操作系统如何处理这些棘手的情况。操作系统可能简单地决定,在中断处理期间禁止中断(disable interrupt)。这样做可以确保在处理一个中断时,不会将其他中断交给CPU。当然,操作系统这样做必须小心,禁用中断时间过长可能导致丢失中断,这(在技术上)是不好的。

操作系统还开发了许多复杂的加锁(locking)方案,以保护对内部数据结构的并发访问。这使得多个活动可以同时在内核中进行,特别适用于多处理器。

小结

我们已经描述了一些实现CPU虚拟化的关键底层机制,并将其统称为受限直接执行 (limited direct execution)。基本思路很简单:就让你想运行的程序在CPU上运行,但首先确保设置好硬件,以便在没有操作系统帮助的情况下限制进程可以执行的操作

通过类的方式,OS首先(在启动时)设置陷阱处理程序并启动时钟中断,然后仅在受限模式下运行进程,以此为CPU提供“防护”。这样做,操作系统能确信进程可以高效运行, 只在执行特权操作,或者当它们独占CPU时间过长并因此需要切换时,才需要操作系统干预。

至此,我们有了虚拟化CPU的基本机制。但一个主要问题还没有答案:在特定时间, 我们应该运行哪个进程?调度程序必须回答这个问题,因此这也是我们研究的下一个主题。

总结

现在讲完了操作系统对进程的抽象,进行的一些重要API以及进程的受限执行,接下来要学习操作系统是使用什么策略来对进程进行调度的,想学习进程调度策略的小伙伴,可以参考我的另一篇博客,其中对各种调度策略以及他们的优缺点进行了详细的讲述:
操作系统对CPU的虚拟化——进程调度策略

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

操作系统对CPU的虚拟化——进程的抽象、进程相关API以及进程的受限执行 的相关文章

  • 线程和进程的区别(面试必备)

    参考文章 https www jianshu com p 2dc01727be45 线程与进程的区别通俗的解释 https www jianshu com p 8ad441510860 附加可参考文章 https baijiahao bai
  • 掉电无法启动数据库问题解决

    由于突然掉电 造成客户在windows平台上10 2 0 1数据库无法驱动 以下是具体解决步骤 一 定位故障问题 1 启动数据库 查看错误 SQL gt startup ora 01113 file 1 needs media recove
  • VMware-Ubuntu安装bochs

    我的运行环境是VMware的Ubuntu 首先大家可以按照CSDN上的教程按照符合自己需求的虚拟机 我在上午还在VMware和virtualBox之间做选择 但是由于已经安装过了VMware 所以我就直接用了VMware 当然了 一千人眼中
  • Linux网络安全-Zabbix入门(一)

    一 基本概念 1 监控目的 运行情况 提前发现问题 2 监控资源类别 公开 tcp udp 端口 私有 cpu 磁盘 监控一切需要监控的东西 只要能够想到 能够用命令实现的都能用来监控 如果想远程管理服务器就有远程管理卡 比如Dell id
  • mapengpeng1999@163.com 操作系统4~处理机调度

    处理机调度 1 三级调度体系 1 处理机调度主要是对处理机运行时间进行分配 即 按照一定算法或策略 将处理机运行时间分配给各个并发进程 同时尽量提高处理机的使用效率 2 现代操作系统中 按调度所实现的功能分3种类型 高级调度 中级调度和低级
  • Client-Server问题

    1 实验内容与要求 需要创建客户Client和服务器Server两个进程 它们通过管道进行通信 Client进程派生3个生产者线程 一个管道线程 共享一个20个slots的缓冲区 每个生产者线程随机产生一个数据 打印出来自己的id 进程 线
  • Linux 磁盘与文件系统管理(鸟哥私房菜)

    本文来自 http vbird dic ksu edu tw linux basic 0230filesystem php 第八章 Linux 磁盘与文件系统管理 系统管理员很重要的任务之一就是管理好自己的磁盘文件系统 每个分割槽不可太大也
  • Linux使用nvida-smi查看GPU类型

    nvida smi提供一个查看GPU信息的方法 然而这种方式不能查看GPU型号 型号被省略成了GeForce RTX 208 如果我们需要查看GPU的型号 只需要运行nvidia smi L即可 mrfive ubuntu nvidia s
  • Java堆的自动垂直缩放

    多年以来 java一直是贪婪的应用程序的同义词 这种类型的应用程序在晚上打开冰箱并吞噬所有可用资源 直到崩溃 该行为的主要原因是缺乏一种有效的方式来将操作系统在Java堆中分配且不再使用的内存交还给操作系统 However with the
  • LWIP在STM32上的移植

    本文做记录摘抄 加上自己的体会 文章标题 STM32使用LWIP实现DHCP客户端 http www cnblogs com dengxiaojun p 4379545 html 该文章介绍了几点 LWIP源码的内容 关键点 1 inclu
  • Elasticsearch 日志

    下载并安装 Filebeat 首次使用 Filebeat 请参阅入门指南 复制代码片段 curl L O https artifacts elastic co downloads beats filebeat filebeat 7 2 0
  • 操作系统 段页式存储管理

    一 引入 分页系统是以页面作为内存分配的基本单位 能有效地提高内存利用率 但信息共享等不方便 分段系统是以段作为内存分配的基本单位 它能够更好地满足用户多方面的需要 信息共享 动态链接等 但采用分区方式管理物理内存 仍然存在碎片问题 段页式
  • 通过源码包*.src.rpm定制开发rpm

    为什么80 的码农都做不了架构师 gt gt gt 1 基本流程 1 下载 安装相应的src rpm包 wget xxx src rpm rpm ivh xxx src rpm 这里的 安装 是指把xxx src rpm中的tar gz p
  • Windows驱动开发(一)第一个驱动程序

    首先我们需要了解 在操作系统中 是分两种权限的 一种是内核态 我们也称为0环 一种是用户态 称之为3环 而在我们的电脑中 驱动程序是运行在内核态的 这意味着和操作系统内核是在同一权限的 而普通的应用程序的权限是最低的 高权限谁不想拥有呢 因
  • Linux 内核中的 Device Mapper 机制

    Linux 内核中的 Device Mapper 机制 尹 洋 在读博士生 尹洋 中科院计算所国家高性能计算机工程技术研究中心的在读博士生 主要从事服务部署和存储资源管理以及Linux块设备一级的开发和研究工作 简介 本文结合具体代码对 L
  • Ubuntu9.04太多乱码(中文不能正常显示)

    最近在使用Ubuntu9 04的过程中 发现有好多地方都出现乱码 其实是中文不能正常显示 现在把我所遇到的所有乱码问题集中一下 方便以后查阅参考 一 Flash乱码 在终端输入 sudo gedit etc fonts conf d 49
  • 图解五种磁盘调度算法, FCFS, SSTF, SCAN, C-SCAN, LOOK

    一 FCFS 调度 先来先服务 磁盘调度的最简单形式当然是先来先服务 FCFS 算法 虽然这种算法比较公平 但是它通常并不提供最快的服务 例如 考虑一个磁盘队列 其 I O 请求块的柱面的顺序如下 98 183 37 122 14 124
  • 《深入理解计算机系统》实验四Architecture Lab

    前言 深入理解计算机系统 实验四Architecture Lab下载和官方文档机翻请看 深入理解计算机系统 实验四Architecture Lab下载和官方文档机翻 我觉得这个文档对整个实验很有帮助 如果你的Y86 64环境还没安装好可以看
  • 地址映射与共享

    跟踪地址映射过程 1 通过命令 dbg asm启动调试器 在linux 0 11运行test c文件 使其进入死循环 我们的任务就是找到i的地址并将其修改为0使test c程序退出循环 2 在命令行输入crit c使Boch暂停 一般会显示
  • I/O设备模型

    I O设备模型 绝大部分的嵌入式系统都包括一些I O Input Outut 输入 输出 设备 例如仪器上的数据显示屏 工业设备上的串口通信 数据采集设备上用于保存数据的Flash或SD卡 以及网络设备的以太网接口等 I O设备模型框架 R

随机推荐

  • spring boot学习之自定义starter启动器

    starter启动器的目标 引入maven包即可自动装配配置 个人理解 如jdbc引入即可操作数据库 实现 1新建springboot工程编写实现类 2编写配置类 3配置 4打包 4新建工程引入使用
  • 移动端rem适配方案(解决1px 兼容问题)

    div style width 7 5rem height 2rem background red div
  • 使用openssl合成pfx格式证书的国密证书

    目前 openssl也已经开始支持国密协议 这边使用的是openssl 1 1 1k 进行的测试 下面记录一下自己的测试步骤 下载地址 https www openssl org source 安装编译方法参考下面地址 我也是参考下面进行安
  • JAVA学习日记(2)--找出某一个范围内完数

    完数 5分 题目内容 一个正整数的因子是所有可以整除它的正整数 而一个数如果恰好等于除它本身外的因子之和 这个数就称为完数 例如6 1 2 3 6的因子是1 2 3 现在 你要写一个程序 读入两个正整数n和m 1 lt n
  • 入门stm32简单电灯实验

    看原理图找内置LED接线 stm32f103 我这边是接的 PE5 外设时钟使能寄存器的相关配置 因为LED1接的是PE5 所以GBIO端口E 查看中文手册获取GPIOE寄存器起始地址0x4001 1800 通过查看系统架构 可以发现GPI
  • 【开发环境】Windows下搭建TVM编译器

    关于搭建TVM编译器的官方文档 Install from Source tvm 0 14 dev0 documentation apache org 1 安装Anaconda 首先我们需要安装Anaconda 因为其中包含着我们所需要的各类
  • QPalette的详细使用示例

    1 功能简介 QPalette是Qt中的调色板类 它提供的setColor 函数可改变控件的颜色 其原型为 void QPalette setColor ColorRole acr const QColor acolor 其中 ColorR
  • 一篇完整学习JUC并发编程(包含实例源码)

    文章目录 JUC并发编程 1 什么是JUC 2 线程和进程 并发 并行 线程有几个状态 6个 wait sleep区别 3 Lock锁 传统sychronized锁卖票实例 使用Lock锁卖票实例 sychnorized 和 lock锁区别
  • 循序渐进地代码重构

    对于如何进行代码重构 一直有着很多种说法 很多人都认为应该将重构代码放在backlog里 但是其实 这并不是一个理想的方法 在项目刚刚开始的时候 你的代码很干净 即使有的时候需要小小的绕一下路 但是这个时候我们可以轻松 平稳的添加功能 这个
  • sql年月日的取值

    一个月第一天的SQL 脚本 SELECT DATEADD mm DATEDIFF mm 0 getdate 0 本周的星期一 SELECT DATEADD wk DATEDIFF wk 0 getdate 0 一年的第一天 SELECT D
  • 使用eclipse将项目达成war包并部署至服务器

    一 eclipse将ssm项目打成war包 1 右击要打war包的项目 选择Export 2 选中Web文件下的WAR file点击Next 3 点击Browse 选择打war包存放的地址 点击Finish开始打包 将打好的War包部署至服
  • 常用的几款抓包工具

    常用的几款抓包工具 标签 软件测试软件测试方法软件测试学习 原创来自于我们的微信公众号 软件测试大师 最近很多同学 说面试的时候被问道 有没有用过什么抓包工具 其实抓包工具并没有什么很难的工具 只要你知道你要用抓包是干嘛的 就知道该怎么用了
  • Pandas基础操作(上)

    文章目录 一 Pandas文件读取 1 pandas数据读取 1 读取纯文本文件 1 1 读取csv 使用默认的标题行 逗号分隔符 1 2 读取txt文件 自己指定分隔符 列名 2 读取excel文件 3 读取sql文件 二 pandas的
  • Centos7升级内核——图文详尽版

    Linux是一种开源电脑操作系统内核 它是一个用C语言写成 符合POSIX标准的类Unix操作系统 Linux最早是由芬兰 Linus Torvalds为尝试在英特尔x86架构上提供自由的类Unix操作系统而开发的 该计划开始于1991年
  • python函数之可变默认参数

    文章目录 问题剖析 元组的使用 一个 Python Bug 干倒了估值 1 6 亿美元的公司 今天在CSDN首页看到这篇文章 不仅感概 水能载舟 亦能覆舟 作为一家仰仗技术出身的公司 最终却因为技术的问题而断崖式地走向没落 实在令人唏嘘 技
  • 剑指offer 学习笔记 数组中数字出现的次数

    面试题56 数组中数字出现的次数 1 一个整型数组里除两个数字之外 其他数字都出现了两次 找出这两个只出现一次的数字 要求时间复杂度O n 空间复杂度O 1 先分析如果只有一个数字出现一次 而其他数字都出现了两次 我们就可以依次异或数组中的
  • nginx 安装

    1 下载Nginx依赖 wget http nginx org packages centos 7 noarch RPMS nginx release centos 7 0 el7 ngx noarch rpm 2 运行Nginx依赖包 运
  • ValueOperations<String, Object> ValueOperations = redisTemplate.opsForValue();中ValueOperations的所有方法详

    ValueOperations 是 RedisTemplate 提供的用于操作 Redis 缓存中值的接口 它定义了一系列可以使用的方法 以下是 ValueOperations 接口中的常用方法 void set K key V value
  • 制造业ERP采购数字化管理系统的搭建及SRM系统介绍

    在我国社会经济高速发展 科学技术日益进步过程中 各行各业的企业为了达到高效管理 效益提升的目的 在采购管理中充分应用ERP系统 本文主要基于制造业ERP的采购管理系统 同时结合ERP系统的延伸供应商管理系统 对采购系统流程中的采购询价 采购
  • 操作系统对CPU的虚拟化——进程的抽象、进程相关API以及进程的受限执行

    因为我们计算机的cpu处理器数量是有限的 但是操作系统理论上可以同时并发的运行无数个进程 那么cpu内部是如何实现的呢 操作系统是通过对CPU进行虚拟化来实现的 本篇博客主要讲述操作系统对CPU的虚拟化中的进程相关的知识 包括进程的抽象 进