linux shell 运行后没有那个文件或目录,需要怎么改?

本文从一个简单hello程序出发,从它的预处理、编译、汇编、链接到加载内存、运行和发送信号过程来整体上理解一个程序在计算机系统里是怎么度过“这一生”的,了解了一个hello程序简单而又复杂的一生,通过对这些过程的理解,深入了解汇编、ELF文件、shell、虚拟内存、动态内存分配及IO管理的相关知识,从一个hello程序的生命周期出发,深入理解现代计算机系统(包括操作系统)与程序的关系,让人不禁赞叹于计算机系统这一现代工程奇迹

关键词:hello程序计算机系统;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

现代程序员无论入门哪一种编程语言hello都是接触的最基础且最简单的程序。了解hello这个程序经过预处理,编译,汇编,链接等过程最后成为可执行目标文件的过程,对于每一名程序员来说无疑是重要的。

对于hello的一生,要从编写hello.c源代码/程序加载进内存开始。一个极其简单的hello程序,实现了P2P020的过程。

Zero-0)在Linux操作系统中shell调用execve加载运行hello程序,将其映射到虚拟内存,然后载入物理内存,进入 main函数执行hello目标代码(功能),CPU为hello程序执行逻辑控制流。shell父进程调用waitpid待hello运行结束后回收hello进程。

在存储管理中,链接的动态优化、虚拟内存背后MMU(Memory Management Unit,内存管理单元)通过TLB(快表)和4级页表(64位)为hello进行VA到PA等的翻译工作,运行时使用三级cache(CPU)进行缓存,提升运行效率。

在IO管理和信号处理中,各种操作亦在有条不紊地执行。看似简单的hello程序反映的不仅是简单的外观表现,更是一个庞大充满各种细节的计算机系统(硬件+OS)。了解hello的一生,让人不禁赞叹于计算机系统这一现代工程奇迹。

hello.o:hello.s汇编生成的可重定位目标文件

hello:hello.o经链接生成的可执行目标文件

上述各文件见附件(实际文件)

本章从一个简单的hello程序入手,从P2P和020方面对hello程序进行了简单介绍,初步展开了接下来将细致讨论的内容,相当于研究纲领,对每个任务进行了简要概括。那么接下来探索hello的一生吧

概念:预处理是在编译步骤之前进行的处理。ISO C规定程序由源代码被翻译分为若干有序的阶段(phasee),通常前几个阶段由预处理程序或者说预处理器来完成,它可以是一个独立的程序,也可以不是。预处理的主要任务是执行预处理指令。预处理指令包括条件包含、源文件包含、宏替换、行控制、抛错、杂注和空指令

每个预处理指令以预处理记号“#”开头,后面跟着一系列其他预处理记号,最后结束于“#”后面的第一个换行符。但是,前面带“#”的并非都是预处理指令,只有在以下两种情况下,以“#”引导的记号序列才是预处理指令:

(1)预处理记号“#”的前面没有其他字符,或只有空白。在这种情况说明“#”出现在源文件的开头。

(2)预处理记号“#”的前面是空白,且这些空白中至少有一个换行符。在这种情况下,“#”位于新的一行上,但不一定是该行的第一个字符。[1]

C语言的预处理主要有以下内容:

)指令的功能是搜索指定的文件,并将它的内容包含进来,放在它当前所在的位置。组成文件名的字符必须是源字符集的成员,是否区分大小写由C实现自行决定。通常情况下,它是数字字符或非数字字符组成的序列,外加一个“.”,以及任何一个单一的非数字字符,如bailig135.h。文件可能会出现彼此包含的情况。在这种情况下,同一个文件可能会在某个源文件内被包含多次。显然,重复出现的内容(如标识符)会在程序转换期间被视为非法。避免发生这种情况的做法是使用条件包含。

宏的作用是把一个标识符(预处理记号)指定为其他一些称为替换列表的预处理记号,当这个标识符出现在后面的文本中时,将用对应的替换列表把它替换掉。可以定义两种形式的宏,分别是对象式宏、函数式宏。替换列表前后的空白和替换列表没有关系,它们不是替换列表的组成部分,也不会随替换列表一起出现在被替换的位置。宏定义中的替换列表可以为空;宏名可以是关键字。但一般不建议(除非确实需要)。宏不允许重复定义,除非它们的替换列表完全等价,对于函数式宏定义来说,它们的参数在数量上相同,对应的标识符拼写一致。两个替换列表完全等价的前提是它们包含相同数量的记号序列,顺序和拼写一致,均以空白分隔。

宏定义的可见性是从它定义之后开始的,一直到用#undef指令取消定义。如果未取消定义,则可见性延伸到当前源文件末尾。可以用#undef指令取消先前的宏定义。该指令的格式为“#” 及“undef”,后面跟一个标识符。如果该标识符以前被定义为宏,则它不再是一个宏的名称;否则无任何效果。

一旦替换列表中的形参被替换,且替换列表中的#和##也处理完成,则预处理器将重新扫描结果序列及后续的源文件内容,以期找到其他可以替换的宏名。即使宏在扩展后的结果是一个有效的预处理指令,它也不会被执行(除了预处理运算符_Pragma)。

条件包含指令的功能是根据条件有选择性地保留或者放弃源文件中的某些内容,它们以#if、#elif、#ifdef或者#ifndef指令开始,结束于#endif,中间可以有#else和#elif指令,但每个完整的条件包含指令内部只能有一个#else指令。#if和#elif后面的预处理记号在宏扩展之后应当是一个常量表达式,也称控制表达式。但如果此记号是预处理运算符defined的操作数,则不执行宏扩展。在宏扩展时,凡是没有定义过的标识符都被替换为“0”。预处理运算符defined只能出现在#if和#elif指令中

#if、#elif、#ifdef和#ifndef指令的处理过程如下求值控制表达式或者预处理运算符defined,若其值为1,则保留同它直接关联的行组,其余到#endif为止的行组都放弃若控制表达式的值为0,且存在#elif指令,则依次求值每个#elif指令,直到其中有一个控制表达式的求值为1,且保留和该指令直接隶属的行组,放弃其他#elif指令的行组若所有控制表达式的值都为0且存在#else指令,则保留该#else指令直接关联的行组,放弃剩余行组如果没有#else指令,则放弃该#if或者#elif指令的所有行组。

在程序转换阶段,如果源代码有错误,则将终止转换,并给出诊断信息,通常还会给出错误所在行的行号。但是,如果源文件在提交给预处理器之前被其他工具加工过,则这些工具可能会修改源文件,在其中加入自己生成的内容。在这种情况下,当程序转换失败时,所提示的行号相对于看到的源文件来说是不正确的。为了避免这种情况,在将源文件提交给整个编译过程之前,可以在那些要被其他软件处理的文本后面使用行控制指令。行控制指令以“#” 引导,后面是行号和可选的字面串。行控制指令将下一行的行号指定为用户给出的值。行号必须是十进制整数,不可以是0,也不可以大于它用于改变预定义宏_LINE_的值如果字面串存在,则用于改变预定义宏_ FILE_的值。如果在“line”之后的预处理记号中存在宏,则先扩展这些宏,再执行行控制指令。

抛错指令以#error引导,后面是其他预处理记号,通常是错误信息。抛错指令用于在预处理期间发出一个诊断信息,再停止转换。抛错是人为的动作,如果你的源程序需要一些特殊的条件才能转换,比如,要求int 类型的长度至少是32位,或者使用了一些新特性,但不确定编译器是否支持,都可以用预处理指令加以判断,如果不符合要求,则停止编译,并通过抛错指令告诉操作员具体的原因。任何时候,只要执行了抛错指令,程序的转换过程必须停止,而且程序的转换必以失败告终。

杂注指令用于向C实现传递额外的信息(编译选项),对程序转换的某些方面进行控制。一般地,如果这些选项是由C实现提供的,则它可以识别。杂注指令以“#”开始,跟着“pragma”,后面是其他预处理记号,即所谓的选项。标准杂注不执行宏替换。无法使用宏扩展生成#pragma指令。如果有这种需求的话可使用C99新增的一元预处理指令_Pragma用来建立一个杂注表达式。

空指令只有一个“#”,自成一行。也就是说在结束该指令的换行符之前,除了空白和“#”之外不存在其他任何记号。空指令的使用没有任何效果。

作用:通过预处理生成的预处理文件(hello.i)是一个可读但不包含任何宏定义 的文件。其具体作用表现为:

2. 插入头文件到“#include”处,可以递归方式进行处理。

4. 添加行号和文件名标识,以便编译器产生调试用的行号信息。

经对比可发现由hello.c经预处理得到的hello.i文件的代码风格仍类似于c语言代码风格但其内容明显变多,由原来的23(.c)变为3069(.i)其文件大小由原来的527B(.c)扩为65.6kB(.i)。并且生成的hello.i文件中宏定义,特殊符号,条件包含指令。原来的宏定义输出文件hello.i中被进行宏替换和宏展开,头文件(*.h)中的内容通过一系列递归替换操作添加到该文件中。

本章介绍了预处理过程中的若干内容、hello.c经预处理输出生成hello.i文件的过程,通过执行“cpp hello.c > hello.i”命令而得,对hello.i的内容进行了解析。

概念:编译是对hello.c经预处理输出生成的预处理文件(hello.i)进行编译操作生成汇编代码文件的过程。具体表现

1. 检查代码的规范性,是否有语法错误等,以确定代码实际要做的工作

作用:将经过预处理得到的预处理文件翻译为汇编语言格式的汇编文件(.s)

经过编译,预处理文件hello.i生成了汇编语言文件hello.s。

首先观察hello.s文件main之前的部分

3.3.1汇编文件指令:

:字符串0“用法: Hello 学号 姓名 秒数!”对应编码

hello.s中c语言的数据类型有全局变量,局部变量,指针数组,字符串

-20(%rbp);cmpl是比较指令cmpl的功能相当于减法指令(sub)它不保存结果只影响相应的标志位。随后的je .L2根据标志位判断是否需要跳转到.L2

%rbp”相对应,用来平衡栈帧。

编译是非常复杂的过程,不单是从.i文件生成输出.s文件那么简单。对于这个简单的hello函数,编译器进行词法分析语法分析和语义分析,在确认所有指令都符合语法规则之后,才能将其翻译成汇编代码,最终生成.s汇编文件。

概念:汇编器将汇编语言源程序转化为机器指令序列的过程叫做汇编。汇编器将hello.s翻译成机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果输出为可重定位文件hello.o。hello.o文件是一个二进制文件,它包含程序的指令编码。

作用:将编译后的.s文件中的汇编语言指令转化为机器语言指令并将结果输出为.o可重定位目标程序文件。

以一个16字节的Magic序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器分析语法和解释目标文件的信息,其中包含ELF头大小、目标文件的类型、机器类型、字节头部表的文件偏移由上图可知hello.o是REL(可重定位)类型的文件。

描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

一个.text节中位置的列表,包含.text节中需要重定位的信息,需要使用链接器在组合时将这些位置链接。hello.o中的getchar,exit等的重定位信息。每一列都包含:

Offset:需要重定向文件在.text或者.data中的偏移量。

Type:重定向的目标类型

Sym.name:重定向到目标名称

Addend:重定向位置的辅助信息

存放在程序中定义和引用的函数和全局变量的信息

通过对比反汇编得到的汇编代码和.i文件中的汇编代码,可得出如下结论:

(1)分支转移部分:hello.s中对跳转指令使用.Lx这样的名称,而hello.o中

的反汇编中的跳转是直接使用跳转地址。

(2)函数调用:hello.s中的函数调用只写了函数名称,hello.o的反汇编中则   是使用了当前指令的下一个字节(即下一条指令的地址)。

(4)立即数:hello.s中的立即数是十进制的,而hello.o的反汇编中则是十六   进制的。

(5)汇编代码中有很多“.”开头的伪指令用来指导汇编器和链接器工作,而   反汇编得到的代码中则没有。

本章了分析hello.o的elf文件格式信息,通过查看hello.o的反汇编代码,从众多方面比较了与hello.s内容的区别,为下一步的链接做准备。

概念:链接是指将可重定位目标文件经符号解析和重定位步骤合并成可执行目标文件,这个文件可被加载到内存并执行,在本报告中是指从hello.o文件生成hello的过程。

作用:使得一个项目可以被分解成多小模块,每个模块可单独进行修改,最后通过链接形成一个项目,实现了分离编译。

相比于可重定位目标文件hello.o,可执行目标文件hello类型从REL(可重定位文件)转变为了EXEC (可执行文件)。ELF头描述hello文件的总体格式。它还包括程序的入口点(entry point)(非0),也就是当程序运行时要执行的第一条指令的地址。

可执行目标文件的格式类似于可重定位目标文件的格式。.text、.rodata和.data节与可重定位目标文件中的节相似,除这些节已经被重定位到它们最终的运行时内存地址以外。.init节还定义了一个叫做_init的小函数,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),所以它不再需要.rel节。

符号表是一个条目数组。这些条目包括目标位置的偏移量(Value),目标大小(Size)Type表示数据或者函数,Bind表示符号是local还是global的

使用edb加载hello,通过Data Dump窗口可查看进程虚拟地址空间的各段信息。

通过点击Plugins菜单中的SymbolsViewer选项调出Symbols窗口以查看进程中各个符号的信息,实际信息如图24所示。从图中可知程序入口点_start的地址真如ELF头中所示在0x4010f0处(.text代码节首地址);其他符号类似。

观察hello的反汇编代码,与hello.o的反汇编代码进行对比发现如下不同点:

2. 节:hello的反汇编代码中增加了.init节和.plt节和一些节中定义的函数。

4. 地址:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。

从加载hello到_start,到call main,以及程序终止的所有过程。其调用与跳转的各个子程序名或程序地址如下:

GOT起始表位置为0x404000,当程序调用共享库定义的函数时,编译器无法预测这个函数的运行时地址,定义它的共享模块可以在运行时加载到任何位置。首先为该函数生成一条重定位记录,然后动态链接器在程序加载时解析。GNU编译系统使用了后期延迟技术,将过程地址的绑定延迟到第一次调用。

本章主要介绍了链接的概念和作用,链接是指将可重定位目标文件经过符号解析和重定位过程后合并成可执行文件,是将各种代码和数据片段整合成一个整体的结构并且将其有序组织的过程。在生成、加载、运行时均能进行链接。分析了可重定位文件和可执行文件的不同,分析了hello的虚拟地址空间、重定位过程、执行过程以及动态链接过程。

概念:进程是一个执行中的程序的实例;是一个具有一定独立功能的程序关于某个数据集合的一次运动活动;是系统进行资源分配和调度运行的基本单位。

作用:进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。

在Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。其基本功能是解释并运行用户的指令,重复以下处理流程:

(1) 终端进程读取用户由键盘输入的命令行。

(2) 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。

(3) 检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令。

(4) 如果不是内部命令,则调用fork()创建新进程/子进程。

(5) 在子进程中,用步骤2获取的参数调用execve()执行指定程序。

(7) 如果用户要求后台运行(如果命令末尾有&号),那么shell返回并继续等待 用户的下一次输入。

在shell中键入命令时,shell分析输入的命令并解释执行hello程序,使用fork创建子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但独立)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。父进程和子进程是并发运行的独立进程,而内核能够以任何方式交替执行它们逻辑控制流中的指令。

execve函数加载并运行可执行目标文件hello,并且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello的时候execve才会返回到调用程序。execve加载完hello之后调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数main()。

结合虚拟内存和内存映射过程,可以更加详细地说明exceve函数实际上是如何加载和执行程序hello的,加载并运行hello需要以下几个步骤:

1. 上下文信息:内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。

2. 进程时间片:一个进程执行它的控制流的部分的每时间段

3. 用户模式和内核模式:为使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器中的一个模式位(modebit)来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令(privileged instruction),比如停止处理器、改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回用户模式[2]

hello一开始运行在用户态,当它调用函数(系统调用)sleep()进入内核态。它显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。

hello执行过程中可能产生如下异常:

中断:来自外部I/O信号,异步异常

陷阱:有意的,是执行指令的结果同步异常 

31  程序运行过程中键入的各种命令及其反馈

1. 运行中乱按:不影响程序的执行,按到回车后,getchar读入回车符, 然后其后的字符串当做shell的命令行输入。

3. 运行中按下ctrl+z后运行jobs/ps命令:父进程(shell)会收到SIGTSTP 信号,然后挂起hello进程,jobs命令显示系统中的任务列表及其运行 状态(图31中的已终止应为翻译错误)。ps命令显示当前进程的状态。

6. 运行中按下ctrl+z后运行相应kill命令(图32):显示Linux系统中的进 程状态树,用ASCII字符以树状结构显示正在启动或执行的程序间的 相互关系。从图32(下页)中可看出hello进程的父进程链为:

本章介绍了进程的概念与作用,简述了壳(shell)的作用与处理流程、通过fork和execve执行hello进程,内核对hello进程的调度、hello进程执行期间可能遇到的异常情况及其响应。

逻辑地址:由程序产生的与段相关的偏移地址部分,又称绝对地址。例如在C语言指针编程中能读取指针变量的值(&操作),这个值就是逻辑地址,它是相对于当前进程数据段的地址。只有在Intel实模式下逻辑地址才和物理地址相等。

线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址就是物理地址。

虚拟地址:现代处理器使用一种称为虚拟寻址(virtual addressing)的寻址形式。使用虚拟寻址,CPU通过生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。

物理地址:目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

段式管理是实现逻辑地址到线性地址转换机制的基础,段的特征有段基址、段限长、段属性。这三个特征存储在段描述符中,用以实现从逻辑地址到线性地址的转换。段描述符存储在段描述符表中,通常使用段选择符定位段描述符在这个表中的位置。每个逻辑地址由16位的段选择符和32位的偏移量组成。段基址规定了线性地址空间中段的开始地址。在保护模式下,段基址长32位。因为基址长度和寻址地址的长度相同,所以段基址可以是0 ~ 4GB范围内的任意地址。

段描述符是GDT(全局描述符表)和LDT(局部描述符表)中的一个数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。通常由编译器,链接器,加载器,或操作系统或执行体(不由应用程序创建)。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里所有的描述符都在一起集中存放,这就构成了一个描述符表。最主要的描述符表是全局描述符表(Global Table,GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须先定义全局描述符表。为了跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符表寄存器(GDTR)。该寄存器分为两部分,分别是32位的线性地址和16位的边界。32位的处理器具有32根地址线,可以访问的地址范围是0x ~ 0xFFFFFFFF共232字节,即4GB内存。所以GDTR的32 位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。理论上全局描述符表可以位于内存中的任何地方。但由于在进入保护模式之后处理器立即要按新的内存访问模式工作,所以必须在进入保护模式之前定义GDT。由于在实模式下只能访问1MB的内存,故GDT通常都定义在1MB以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义GDT。

为了有效地在任务之间实施隔离,处理器建议每个任务都应当具有自己的描述符表,称为局部描述符表LDT(Local Descriptor Table),并且把专属于自己的那些段放到LDT中。和GDT一样,LDT也是用来存放描述符的。不同之处在于LDT只属于某个任务。或者说每个任务都有自己的LDT,每个任务私有的段,都应当在LDT 中进行描述。为了追踪全局描述符表(GDT),访问它内部的描述符,处理器使用了GDTR寄存器。正如其名称所暗示的那样,全局描述符表(GDT)是全局性的,我们只需要一个全局描述符表(GDT)就足够了。和GDT不同,局部描述符表(LDT)的数量则不止一个,具体有多少,视任务的数量而定。为了追踪和访问这些LDT,处理器使用了局部描述符表寄存器(LDT Register:LDTR)。在一个多任务的系统中,会有很多任务在轮流执行,正在执行中的那个任务称为当前任务(Current Task)。因为LDTR寄存器只有一个,所以它只用于指向当前任务的LDT。每当发生任务切换时,LDTR的内容被更新,以指向新任务的LDT。和GDTR一样,LDTR包含了32位线性基地址字段和16位段界限字段,以指示当前LDT的位置和大小。

在一个多任务的环境中,当任务切换发生时,必须保护旧任务的运行状态,或者说是保护现场,保护的内容包括通用寄存器、段寄存器、栈指针寄存器ESP、指令指针寄存器EIP、状态寄存器EFLAGS等。为了保存任务的状态,并在下次重新执行时恢复它们,每个任务都应当用一个额外的内存区域保存相关信息,叫做任务状态段(Task State Segment:TSS)。和LDT一样,处理器用TR寄存器来指向当前任务的TSS。这个寄存器叫TR而不是TSSR的缘由:TSS是一个任务存在的标志,用于区别一个任务和其他任务。所以叫做任务寄存器(Task Register:TR)。和GDTR、LDTR一样,TR寄存器在处理器中也只有一个。当任务切换发生的时候,TR寄存器的内容也会跟着指向新任务的TSS,过程为:首先,处理器将当前任务的现场信息保存到由TR寄存器指向的TSS;然后再使TR寄存器指向新任务的TSS,并从新任务的TSS中恢复现场。[3]

如图33所示,从逻辑地址到线性地址的变换过程为:给出一个完整的逻辑地址[段选择符:段内偏移地址]。首先看段选择符判断当前转换时GDT中的段还是LDT中的段,再根据相应寄存器得到其地址和大小。之后拿出段选择符中的前13位,在对应地址中查找到对应的段描述符,这样就知道了基址。根据基址和偏移量结合,就得到了所求的线性地址。

CPU在访问主存前须先将虚拟地址转换成物理地址。MMU(内存管理单元,Memory Management Unit)位于处理器内核和连接高速缓存以及物理存储器的总线之间。当CPU内核取指令或者存取数据的时候,都会提供一个有效地址(effective address,或者称为逻辑地址、虚拟地址)。这个地址是可执行代码在编译的时候由链接器生成的。MMU利用存放在主存中的页表将虚拟地址翻译成物理地址。CPU会通过这个物理地址来访问主存(物理内存)。

页表结构:在物理内存中存放着一个叫做页表的数据结构,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表就是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n位地址字段组成的。有效位表明该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应物理页的起始位置。如果没有设置有效位, 那么一个空地址表示这个虚拟页还未被分配。否则这个地址就指向该虚拟页在磁盘上的起始位置。

0、VPN1选择PTE1,依此类推。将页表条目中的物理页号(Physical PageNumber,PPN)和虚拟地址中的VPO串联起来,就得到了相应的物理地址。因为物理和虚拟页面都是P字节的,所以物理页面偏移(Physical Page Offset,PPO)和VPO是相同的。

每次CPU产生一个虚拟地址,MMU就必须查阅其相应的PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取1次数据,代价是几十到几百个周期。如果PTE恰巧缓存在L1中,那么开销就下降至1个或2个周期。然

而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。TLB通过虚拟地址VPN部分进行索引,分为TLB索引(TLBI)与TLB标记(TLBT)两个部分。如果TLB有T=2t个组,那么TLBI由VPN的t个最低位组成,而TLBT由VPN中剩余的位组成。MMU在读取PTE时会直接通过TLB,不命中时再从内存中将PTE复制到TLB。图35展示了当TLB命中时(通常情况)所包括的步骤。所有的地址翻译步骤都是在CPU上的MMU中执行的,因此非常快。

第1步:CPU产生一个虚拟地址。

第2步和第3步:MMU从TLB中取出相应的PTE。

第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速 缓存/主存。

第5步:高速缓存/主存将所请求的数据字返回给CPU。

到目前为止,我们一直假设系统只用一个单独的页表来进行地址翻译。但是如果我们有一个32位的地址空间、4KB的页面和一个4字节的PTE,那么即使应用所引用的只是虛拟地址空间中很小的一部分,也总是需要一个4MB的页表驻留在内存中,造成了内存的浪费。对于地址空间为64位的系统来说,问题将变得更复杂。所以虚拟地址到物理地址的转换过程中还存在多级页表的机制:上一级的页表映射到下一级页表,直到页表映射到虚拟内存,如果下一级内容都未分配,那么页表项则为空,不映射到下一级,也不存在下一级页表,当分配时再创建相应页表,从而节约内存空间。

i7实现支持48位虚拟地址空间和52位物理地址空间,使用4KB的页。CPU上的PTE为64位,所以每个页表一共有512个条目。512个PTE条目需要9位VPN定位。再四级页表的条件下,一共需要36位VPN,因为虚拟地址空间是48位,故低12位是VPO。TLB四路组相联,共有16组,需要4位TLBI,故VPN的低4位是TLBI,高32位是TLBT。

VA到PA的变换过程:CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT+TLBI向TLB中匹配,命中则得到40位PPN和12位VPO并组合成52位物理地址PA。若未命中,MMU向页表中查询,CR3确定第一级页表的起始地址,9位VPN1确定在第一级页表中的偏移量,查询出第一部分PTE,依此类推最终在四级页表都访问完后获得PPN,与VPO组合获得PA,并向TLB中更新。

三级Cache原理如下:

(1)首先获取物理地址VA,使用物理地址的CI进行组索引(8路组相联),对8   路的块分别与缓存标记CT进行标志位匹配。若匹配成功且块的valid标志   位为1则命中。然后根据块偏移CO取出数据并返回。

(2)若未到相匹配的块或者标志位为0,则miss。一级cache向下逐级cache,   即二级cache甚至是三级cache中寻找查询数据。然后向上逐级写入cache。

shell通过fork为hello创建新进程。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给hello进程唯一的PID。为了给这个新进程创建虚拟内存,它创建当前进程的mm_struct、区域结构和样表的原样副本。它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好的和调用fork时的虚拟内存相同。而当这两个进程中任何一个进行写操作时,就会触发一个保护故障。当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限,如图38所示。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。通过延迟私有对象中的副本直到最后可能的时刻,写时复制机制最充分地使用了稀有的物理内存,同时也为每个进程保持了其私有地址空间。

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存   在的区域结构。

2. 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结    构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被    映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射    到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,    初始长度为零。图39概括了私有区域的不同映射。

3. 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库    libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户    虚拟地址空间中的共享区域内。

4. 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下    文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进    程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据    页面。

39  加载器是如何映射用户地址空间的区域的

假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

1. 虚拟地址A是合法?缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。实际上Linux使用某些我们没有显示出来的字段,Linux在链表中构建一棵树,并在这棵树上进行查找。

2. 进程是否有读、写或者执行这个区域内页面的权限?例如这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

3. 内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次MMU就能正常地翻译A,而不会再产生缺页中断。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)(见图41)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做"break"),它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是个连续的虚拟内存片(chunk)要么是已分配的,要么是空闲的。已分配的块显式地保留供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

显式分配器(explicit allocator)要求应用显式地释放任何已分配的块。例如C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

隐式分配器(implicit allocator)要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector)而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

造成堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象,当虽然有未使用的内存但不能用来满足分配请求时,就发生这种现象。有两种形式的碎片内部碎片(internal fragmentation)和外部碎片(external fragmentation)。内部碎片是在一个已分配块比有效载荷大时发生的。在任意时刻,内部碎片的数量只取决于以前请求的模式和分配器的实现方式。外部碎片是当空闲内存合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。外部碎片比内部碎片的量化要困难得多,因为它不仅取决于以前请求的模式和分配器的实现方式,还取决于将来请求的模式。因为外部碎片难以量化且不可能预测,所以分配器通常采用启发式策略来试图维持少量的大空闲块,而不是维持大量的小空闲块。

任何实际的分配器都需要一些数据结构允许它区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌块本身。一个简单的方法如图42所示。

1. 隐式空闲链表分配器我们可以将堆组织为一个连续的已分配块和空闲块的序列,空闲块是通过头部中的大小字段隐含地连接着的,这种结构为隐式空闲表。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块集合。一个块由一个字的头部、有效载荷、可能的填充和一个字的脚部组成,其中脚部就是头部的一个副本。头部编码了这个块的大小以及这个块是已分配还是空闲的。分配器就可以通过检查它的头部和脚部,判断前后块的起始位置和状态。

2. 显示空闲链表分配器将堆组成一个双向空闲链表,在每个空闲块中都包含一个pred(前驱)和succ(后继)指针。一种方法是用后进先出(LIFO)的顺序来维护链表,将新释放的块放置在链表开始处。使用LIFO的顺序和首次适配放置策略,分配器会最先检查最近使用过的块。在这种情况下释放一个块可以在常数时间内完成。使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于后继地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用。显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致更大的最小块大小,潜在提高了内部碎片的程度。

本章讨论了存储器地址空间、段式管理、页式管理,以Core i7为例介绍了VA到PA的变换、还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理。加深了对动态内存分配的认识和了解

设备的模型化:所有的IO设备都被模型化为文件,而所有的输入输出都被当做对相应文件的读和写来执行。

设备管理:允许Linux内核引出一个简单、低级的应用接口,将设备优雅地映射为文件的方式称为Unix I/O,这使得所有的输人和输出都能以一种统一且一致的方式来执行。

1. 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数(描述符),用于标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只须记住这个描述符。

3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek操作显式地设置文件的当前位置为。

4. 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当km时执行读操作会出发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的“EOF符号”。

5. 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止,内核都会关闭所有打开的文件并释放它们的内存资源。

1. open:进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open将filename转换为一个文件描述符,并且放回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

2. close:进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

3. read:应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误;返回值0表示EOF;否则,返回值表示的是实际传扫的字节数量。

4. write:应用程序通过调用write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

5. lseek:应用程序通过调用lseek函数,能够显示地修改当前文件的位置。

printf函数代码如下所示:

从上面的vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。

对write进行跟踪:

于是可以直到printf函数执行过程如下:

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向显示器传输每一个点(RGB分量)。

返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。当程序调用getchar时程序就等着用户按键用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)当用户键入回车之后getchar才开始从stdin流中每次读入一个字符用户在按回车之前输入了不止一个字符其他字符会保留在键盘缓存区中等待后续getchar调用读取也就是说后续的getchar调用不会等待用户按键而直接读取缓冲区中的字符直到缓冲区中的字符读完为后才等待用户按键

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。hello实现与外接设备的交互,从外接设备读入数据,最后将信息输出到显示屏幕上过程的实现在本章被解析

1. hello被I/O设备编写,以文件的方式储存在主存(适当时刻存入磁盘)

7. hello在一个时间片中有自己的CPU资源,顺序执行逻辑控制流

10. hello在行过程中会有异常和信号需要处理

只有深入理解现代计算机系统(包括操作系统)的底层逻辑和具体实现才更

加有效地利用这一人类工程系奇迹。

(结论0分,缺失 -1分,根据内容酌情加分)附件

hello.o:hello.s汇编生成的可重定位目标文件

hello:hello.o经链接生成的可执行目标文件

(附件0分,缺失 -1分)

[2]  ()兰德尔E.布莱恩特,大卫R.奥哈拉伦著;龚奕利,贺莲译. 深入理解计算机系统. 北京:机械工业出版社,

(参考文献0分,缺失 -1分)

}

上传文件功能会默认将您的文件上传到 Cloud Shell 实例的$HOME 目录下。...

网站程序的目录用于上传文件时,建议您将目录权限设置为可读可写(禁止脚本执行),可降低木马、病毒恶意文件上传后被执行的风险。如果对网站程序的文件或目录设置相应权限后,网站访问出现异常,您可以将对应的文件或目录恢复为初始默认...

脚本文件管理包括新建脚本文件、编辑脚本文件执行脚本文件。在多方安全分析控制台,您需要使用 SQL 语言编辑脚本文件,并基于建模数据实现多方参与的数据安全分析。新建脚本文件登录多方安全分析控制台,并在右上角的下拉框选择需要...

本文介绍了在联合分析场景,多方安全分析控制台的操作流程。查看当前项目的分析样本和样本的安全配置,请参见 查看数据安全配置。...执行脚本文件,请参见 执行脚本文件。获取执行脚本文件后的结果,请参见 查看执行结果。

脚本类型输入配置输出配置创建联邦表原始样本(多个)联邦表(一个多个)预处理规则联邦表(一个多个)联邦表(一个多个)预处理应用联邦表(一个多个)联邦预处理规则(一个)联邦表(一个多个)模型开发联邦表(一个多个)...

您可以使用FTP客户端管理云虚拟主机上的网站文件,也可以使用Linux操作系统云虚拟主机管理控制台文件管理器来管理网站文件。本文为您介绍使用文件管理器管理网站文件的方法。前提条件 已自行准备网站文件到本地主机。背景信息

初始访问 执行 持久化 防御规避 发现 命令与控制 供应链攻击(开启供应链下载安装监控)计划任务作业(禁止下载脚本执行主机相关操作)计划任务作业(禁止下载脚本执行主机相关操作)文件或目录权限变更(禁止下载脚本执行主机相关...

脚本文件夹管理包括新建脚本文件夹、重命名脚本文件夹和删除脚本文件夹。新建脚本文件夹登录多方安全分析控制台,并在右上角的下拉框选择需要进入的项目。在 脚本文件 区域,右键点击空白区域,在弹出的右键菜单选择 新建文件夹,即可...

在联邦建模控制台,您需要通过新建脚本、编辑脚本执行脚本完成联邦建模。操作步骤在联邦建模控制台右上角的下拉框选择需要进入的项目。在 脚本文件 区域,右键单击空白区域,在弹出的右键菜单选择 新建脚本,即可弹出新建文件的...

选中文件列表需要下载的文件或目录,右键选择下载打包下载,可以下载当前选中的文件,如果是目录,下载的是当前打包的目录。压缩文件。按住ctrl键后,单击文件列表文件可以同时选中多个文件,或者单击列表的起始位置按下shift键,...

Zeppelin支持Shell脚本(以%sh开头)。与开源Zeppelin相比,E-MapReduce(简称EMR)数据开发集群的Shell解释器支持在不同EMR集群环境里切换。本文通过示例为您介绍如何在Zeppelin使用Shell。使用示例 运行hadoop命令

脚本编辑完成,通过执行脚本生成联邦建模数据、预处理规则和联邦模型。前提条件已经按照脚本类型,为脚本文件配置了输入数据和输出数据,更多信息请参见 脚本输入/输出配置说明。操作步骤脚本文件编辑完成后,单击页面右上方的 执行,即可...

执行脚本类型为 预处理规则 模型开发 脚本任务后,在其执行结果可以保存脚本执行后产出的预处理规则联邦模型。操作:单击 日志,可以查看该任务的执行日志信息,请参见 查看执行日志。单击 结果 可以查看该任务执行完成后的产出结果...

执行异常(Exception)脚本执行过程出现了异常。常见于您的脚本代码出现了逻辑错误超时,例如传入的参数错误、执行时间超过了50ms等。您可以在ES配置的扩展项中增加_es_dbg配置,并通过CDN控制台EdgeScript页面右上角的调试工具查看...

创建作业作业指提交到E-HPC集群进行高性能计算的基本工作单元,包括Shell脚本、可执行文件等,具体作业执行顺序根据您设置的队列以及调度器决定。您可以在集群使用GROMACS软件运行水分子算例。查看作业结果您可以在E-HPC管理控制台查看...

您无需对业务和镜像进行改造,只需要在Kubernetes的Deployment文件(Pod、Deployment、StatefulSet、Job等都支持)新增一个Sidecar配置,就可以将定时任务托管到SchedulerX平台,拥有白屏运维、可视化和监控报警能力。例如:批量运维:在...

任务执行路径 云助手会将用户的命令内容以文件形式先保存到实例上,然后再执行文件,具体保存路径如下:Linux:/tmp Windows:云助手安装路径/work/script 命令 需要在实例中执行的具体命令操作,如一份Shell脚本或者PowerShell脚本。...

选择 PYTHON 脚本 Shell 脚本 需要添加规则目标,用于校验用户的应用操作权限。业务应用:仅支持业务租户的用户操作。自定义服务器:操作对象无限制。选择 系统插件 需要添加操作符、目标数量以及应用,用于查询应用 POD 数。关联...

引导操作功能可以在集群扩容弹性伸缩自动在新增节点上运行指定脚本。手动执行功能,可以批量选择在已有节点上运行指定脚本,以实现个性化需求,手动执行脚本的详情,请参见手动执行脚本。引导操作类似手动执行,在集群创建...

在场景配置页签下,单击上传文件,添加JMeter脚本、JAR文件(即放置在JMeter的安装目录./lib/ext下的相关JAR文件)和其他数据文件。注意 上传的同名文件将覆盖之前上传的文件。如上传了多个JMX文件,选择一个JMX文件作为该压测场景使用的...

彻底删除目录时目录中的内容会被递归清理。单个文件系统一次只能执行一个彻底删除任务。正在彻底删除文件或目录时,无法发起新的恢复清理任务。调试 您...

单个文件系统最大文件或目录数量 14亿个 文件/目录数量随文件系统容量增长而增加,每1200 GiB容量支持约1500万文件或目录,单个文件系统上限14亿,如需更高要求,请在创建文件系统前,提交工单申请。单个目录下最大文件或目录数量 1亿 单个...

当配额为0仍然允许创建文件,但是不能向该文件中写内容。目录不占用磁盘空间,因此不计入空间配额中。命令示例 管理员命令 管理员可以通过以下命令来管理额度:设置目录的名称配额为N。hdfs

本文介绍如何将告警信息推送至邮箱集成。场景在服务器上产生异常推送至邮箱通知是常见场景,本文介绍将以一段包含关键指标/异常信息的日志推送至邮箱集成为例,在运维...脚本执行时将会把test.log的日志推送到邮件集成。查看邮件集成告警记录

本文介绍在Windows操作系统,如何以AD域身份挂载SMB文件系统。以及挂载成功后,如何以AD域身份访问SMB协议文件系统,查看和编辑文件或目录的ACL。前提条件 SMB文件系统挂载点已接入AD域。具体操作,请参见将SMB文件系统挂载点接入AD域。...

脚本执行目录用于输入执行自定义Shell脚本的本地目录。cocoapods在iOS场景下,用于安装/更新pods依赖。选项说明xcodeproj目录用于设置工程根目录到xcodeporj目录的相对路径。deintegrate

Cloud Shell内置可视化代码编辑器。简化版WebIDE的设计,让...打开后,如下图:2)使用代码编辑器浏览$HOME文件目录以及查看和编辑文件,在页面下方继续使用Cloud Shell。说明 通过 Cloud Shell 绑定存储空间,您编辑的文件可以进行永久存储。

}

我写了个类似cal的脚本mycal,即使是在当前目录下也要这样:./mycal才不会出错
如何让它在任何目录下都可以直接运行,而不需要加路径?

各位所说的我都做到了,现在还是那个问题:
执行cp后,在我自己的bin目录下看不到该脚本。

不过楼上说的倒也是个办法

你不会在自己的bin目录下再写一个mycal脚本吗?

我是远程登陆我们学院开的服务器,在上面做操作系统实验。
现在的问题是执行cp后,在我自己的bin目录下看不到该脚本。
这个bin目录是老师已经为各个用户建好了的,没有权限保护问题,
因为执行cp时,没有出现错误。

一般来说,我都把这样的东西放在/usr/local/bin目录下,常用的系统这个都在path环境中
有些系统不把$home/bin放在path中,所以你在自己的bin目录下放的东西在别的目录下不见得可以执行

试了一下将它cp到我自己的bin目录下,但好像没用。cp后在bin下找不到mycal

cp到/usr/bin中不太好,不如在自己的目录中开一个bin,然后在PATH中加入这个目录。
这样不会污染系统,起码对我们这些小菜们是这样。

把你的脚本cp到/usr/bin等目录下就可以了。:)

}

我要回帖

更多关于 linux简单的shell编程 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信