0%

Python编程备忘录

变量和简单数据类型

变量的命名和使用

(1)变量名只能包含字母,数字和下划线。

(2)变量名不能包含空格,但能使用下划线来分割其中字符。

(3)关键字和函数名不能忘用作变量。

(4)变量名应简单又具有描述性。

字符串

python中单引号和双引号引起来的都是字符串。

name.title()以首字母大写显示。

name.upper()全部大写。

name.lower()全部小写。

用f(format)来合并字符串。name=f“{firstname} {lastname}”

name.strip()来消除空白。该删除为暂时,要永久需关联到变量。

name.rstrip()消除右边空白。

name.lstrip()消除左边空白。

name1=name.rstrip()。

整数

用两个乘号表示乘方运算。3**2=9

将任意两个数相除时,结果总是浮点数,即便这两个数都是整数且能整数。

在其他任意运算中,只要有操作数是浮点数,默认得到的总是浮点数。

给多个变量赋值:x,y,z=0,0,0

用全大写将某个变量视为常量。

列表简介

python中,用[]表示列表,用逗号分隔其中元素。

在python中,索引为负数表示访问倒数第几个元素。

name.append(‘xyz’)在列表末尾添加元素。

name.insert(0,’xyz’)在索引0处插入元素。

用del name[0]永久删除索引0处的元素。

用pop暂时删除末尾元素,加入数字可删除任意位置。name.pop() name.pop(1)

用remove永久删除特定元素。name.remove(‘xyz’)。但是remove只删除第一个指定的值,若多次出现需循环删除。

组织列表

用cars.sort()对列表进行永久排序。

若要按相反的顺序排序,则向方法传递参数reverse=True,cars.sort(reverse.True)

用cars.sorted()对列表进行临时排序。

用cars.reverse()倒着打印列表,且会永久性的修改列表的顺序。

用len(cars)可确定列表的长度。

操作列表

range(1,4)只会打印1 2 3

若range只输入一个参数,则将从0开始。

可给range指定第三个参数,设定为步长。

列表解析

将for循环和创建新元素的代码合并成一行,并自动附加新元素。

squares=[value**2 for value in range(1,11)]

使用列表的一部分

切片

与range函数一样,指定索引,cars[0:3],将返回索引0,1,2的元素

若不指定,cars[:3],将从头开始索引,而cars[3:]将从第四个元素索引到末尾

若访问后三位元素,可cars[-3:]

也可加入第三个值,告诉Python间隔多少提取一个元素

复制列表

省略起始索引与终止索引,cars1=cars[:]

若只是单纯赋值,cars1=cars,则将两个指向同一个列表

元组

python将不可变的列表称为元组

元组使用圆括号而非中括号进行标识

如果你要定义的元组只包含一个元素,必须加上逗号,my_t=(3,)

虽然不能修改元组的元素,但可以给存储元组的变量赋值

dimensions=(200,50)

dimensions=(400,100)

这是合法的,相当于将一个新元组关联到变量dimension

if语句

条件测试

python在检查是否相等时区分大小写

检查多个条件用 and 和 or

要判断特定值是否已包含在列表中,可使用关键字in

检查不包含,可用 not in

字典

在python中,字典是一系列键值对。每个键都与一个值相关联,你可以使用键来访问相关联的值。

字典用在花括号中的一系列键值对表示。

键值对是两个相关联的值,指定键时,Python将返回与之相关联的值。键和值之间用冒号分割,而键值对之间用逗号分割。

要获取与键相关联的值,可依次指定字典名和放在方括号内的值。

对于字典中不需要的信息,可用del语句将相应的键值对彻底删除。使用del语句时,必须要指定字典名和要删除的键。

可使用get来访问值,可避免指定键不存在的情况。

eg:point_value -alien.get(‘points’,’sorry’)

遍历字典

for k,v in users.items()//遍历键值对

for k in users.keys()//遍历键

for v in users.values()//遍历值

用户输入和while循环

要返回循环开头,并根据条件测试结果决定是否继续执行循环,可使用continue。

函数

使用def定义函数

def user():

关键字实参

def pet(type,name)

不使用pet(hamster,harry)

使用关键字实参pet(type=’hamster’,name=’harry’)//无视顺序

默认值

编写函数时,可给每个形参指定默认值,在调用时,若未指定实参,则使用默认形参。

def pet(type=’dog’,name)

传递任意数量的实参

def make(*pizza)://即可接受任意数量的实参

使用任意数量的关键字实参

def make(**pizza)://接受任意数量键值对

将函数存储在模块中

导入整个模块 import pizza

导入模块中特定函数 from pizza import makepizza1,makepizza2

可使用as给模块或者函数指定别名

from pizza import make as m

使用号可导入模块中所有函数 from pizza import

类中的 __init()方法会自动运行,并且开头和结尾各有两个下划线。

继承

class elecar(car):

java集合框架

集合接口与实现分离

与现代的数据结构类库的常见做法一样,java集合类库也将接口与实现分离。下面用我们熟悉的数据结构——队列来说明是如何分离的。

队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”方式检索对象时就应该使用队列。

image-20210630104036075

队列接口的最简形式可能类似下面这样:

1
2
3
4
5
6
public interface Queue<E>
{
void add(E element);
E remove();
int size();
}

这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表。

image-20210630104055147

每一个实现都可以用一个实现了Queue接口的类表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CircularArrayQueue<E> implements Queue<E>
{
private int head;
private int tail;
CircularArrayQueue(int capacity){...}
public void add(E element){...}
public E remove(){...}
public int size(){...}
private E[] elements;
}

public class LinkedListQueue<E> implements Queue<E>
{
private Link head;
private Link tail;

LinkedListQueue(){...}
public void add(E element){...}
public E remove(){...}
public int size(){...}
}

当在程序中使用队列时,一旦已经构造了集合,就不需要知道究竟使用了哪种实现。因此,只有在构造集合对象时,才会使用具体的类。可以使用接口类型存放集合引用。

1
2
Queue<Customer> expresslane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));

利用这种方法,一旦改变了想法,就可以很轻松地使用另外一种不同的实现。只需要对程序的一个地方1做出修改,即调用构造器的地方。如果觉得LinkedListQueue是个更好的选择,就将代码修改为:

1
2
Queue1<Customer> expressLane =new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

为什么选择这种实现,而不选择那种实现呢?接口本身并不能说明哪种实现的效率究竟如何。循环数组要比链表更高效,因此多数人优先选择循环数组。不过,通常来讲,这样做也需要付出一定代价。

循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。

迭代器

Iterator接口包含4个方法:

1
2
3
4
5
6
7
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<?super E> action);
}

通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个可以访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,党hasNext返回true时就反复地调用next方法。例如:

1
2
3
4
5
6
7
Collection<String> c=...;
Iterator<String> iter=c.iterator();
while(iter.hasNext())
{
String element-iter.next();
do something with element
}

用”for each”循坏可以更加简练地表示同样的循环操作:

1
2
3
4
for(String element : c)
{
do something with element
}

编译器简单地将”for each”循环转换为带有迭代器的循坏。

“for each”循环可以处理任何实现了Iterable接口的对象,这个接口只包含一个抽象方法:

1
2
3
4
5
public interface Iterable<E>
{
Iterator<E> iterator();
...
}

访问元素的顺序取决于集合类型。如果迭代处理一个ArrayList,迭代器将从索引0开始,每迭代一次,索引值加1.不过,如果访问HashSet中的元素,会按照一种基本上随机的顺序获得元素。虽然可以确保在迭代过程中能够遍历到集合中的所有元素,但是无法预知访问各元素的顺序。这通常并不是什么问题,因为对于计算总和或统计匹配之类的计算,顺序并不重要。

Java集合类库中的迭代器与其他类库中的迭代器在概念上有着重要的区别。在传统的集合类库中,例如,C++的标准模版库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引i就可以查看数组元素a[i]一样。不需要查找元素,就可以将迭代器向前移动一个位置。这与不需要执行查找操作就可以通过i++将数组索引向前移动一样。但是,Java迭代器并不是这样操作的。查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动。
因此,应该将Java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

image-20210630113409750

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,这是有道理的,在决定删除某个元素之前应该先看一下这个元素。不过,如果想要删除指定位置上的元素,仍然需要越过这个元素:

1
2
3
Iterator<String> it =c.iterator();
it.next;
it.remove();

更重要的是,next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调用next,将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。

如果想删掉两个相邻的元素,不能直接这样调用:

1
2
it.remove();
it.remove();

实际上,必须先调用next越过要删除的元素。

1
2
3
it.remove();
it.next();
it.remove();

具体集合

链表

链表是一个有序集合。

1
ListIterator<E> ListIterator()

返回一个列表迭代器,用来访问列表中的元素。

1
ListIterator<E> listIterator(int index)

返回一个列表迭代器,用来访问列表中的元素,第一次调用这个迭代器的next会返回给定索引的元素。

1
void add(int i,E element)

在给定位置添加一个元素。

1
void addAll(int i,Collection<?extends E>elements)

将一个集合中的所有元素添加到给定位置。

1
E remove(int i)

删除给定位置的元素。

1
E get(int i)

获取给定位置的元素。

1
E set(int i,E element)

用一个新元素替换给定位置的元素,并返回原来那个元素。

1
int indexOf(Object element)

返回与指定元素相等的元素在列表中第一次出现的位置,如果没有这样的元素就返回-1.

1
int lastIndexOf(Object element)

返回与指定元素相等的元素在列表中最后一次出现的位置,如果没有这样的元素将返回-1.

1
void add(E newElement)

在当前位置添加一个元素。

1
void set(E newElement)

用新元素替换next或previous访问的上一个元素。如果在上一个next或previous调用之后列表结构被修改了,将抛出一个IllegalStateException异常。

1
boolean hasPrevious()

当反向迭代列表时,如果还有可以访问的元素,返回true。

1
E previous()

返回前一个对象。如果已经到达了列表的头部,就抛出一个NoSuchElementException异常。

1
int nextIndex()

返回下一次调用next方法时将返回的元素的索引。

1
int previousIndex()

返回下一次调用previous方法时将返回的元素的索引。

1
LinkedList()

构造一个空链表。

1
LinkedList(Collection<?extends E>elements)

构造一个链表,并将集合中所有的元素添加到这个链表中。

1
2
void addFirst(E element)
void addLast(E element)

将某个元素添加到列表的头部或尾部。

1
2
E getFirst()
E getLast()

返回列表头部或尾部的元素。

1
2
E removeFirst()
E removeLast()

删除并返回列表头部或尾部的元素。

散列表

1
HashSet()

构造一个空散列集。

1
HashSet(Collection<?extends E>elements)

构造一个散列集,并将集合中所有元素添加到这个散列集中。

1
HashSet(int initialCapacity)

构造一个空的具有指定容量的散列集。

1
HashSet(int initialCapacity,float loadFactor)

构造一个有指定容量和装填因子的空散列集。

1
int hashCode()

返回这个对象的散列码。

树集

1
TreeSet()
1
TreeSet(Comparator<?super E> compartor)

构造一个空数集。

1
2
TreeSet(Collection<?extends E>elements)
TreeSet(SortedSet<E> s)

构造一个树集,并增加一个集合或有序集中的所有元素。

映射

1
V get(Object key)

获取与键关联的值;返回与键关联的对象,或者映射中如果没有这个对象,就返回NULL。

1
V put(K key,V value)

将关联的一对键和值放到映射中。

1
void putAll(Map<?extends K,?extends V>entries)

将给定映射中的所有映射条目添加到这个映射中。

1
boolean containsKey(Object key)

如果在映射中已经有这个键,返回true。

1
boolean containsValue(Object value)

如果映射中已经有这个值,返回true。

为什么要使用泛型程序设计

泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。

谁想成为泛型程序员

使用像 ArrayList的泛型类很容易。大多数Java程序员都使用 ArrayList这样的类型,就好像它们已经构建在语言之中,像 String[]数组一样。(当然,数组列表比数组要好一些,因为它可以自动扩展。)
但是,实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想要内置( plug in)所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
这一任务难到什么程度呢?下面是标准类库的设计者们肯定产生争议的一个典型问题。 ArrayList类有一个方法addAll用来添加另一个集合的全部元素。程序员可能想要将 ArrayList中的所有元素添加到 ArrayList< Employee>中去。然而,反过来就不行了。如果只能允许前一个调用,而不能允许后一个调用呢?Java语言的设计者发明了一个具有独创性的新概念,通配符类型( wildcard type),它解决了这个问题。通配符类型非常抽象,然而,它们能让库的构建者编写出尽可能灵活的方法。
泛型程序设计划分为3个能力级别。基本级别是,仅仅使用泛型类——典型的是像 ArrayList这样的集合——不必考虑它们的工作方式与原因。大多数应用程序员将会停留在这一级别上,直到出现了什么问题。当把不同的泛型类混合在一起时,或是在与对类型参数一无所知的遗留的代码进行衔接时,可能会看到含混不清的错误消息。如果这样的话,就需要学习Java泛型来系统地解决这些问题,而不要胡乱地猜测。当然,最终可能想要实现自己的泛型类与泛型方法。
应用程序员很可能不喜欢编写太多的泛型代码。JDK开发人员已经做出了很大的努力,为所有的集合类提供了类型参数。凭经验来说,那些原本涉及许多来自通用类型(如 Object或 Comparable接口)的强制类型转换的代码一定会因使用类型参数而受益。

定义简单泛型

泛型类就是有一个或多个类型变量的类。

注释:类型变量使用大写形式,且比较短,这是很常见的。在ava库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。

泛型方法

定义一个带有类型参数的简单方法。

1
2
3
4
5
6
7
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length /2];
}
}

这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

1
String middle ArrayAlg. <String> ("John", "Q", "Public");

在这种情况(实际也是大多数情况)下,方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。它用names的类型(即 String[])与泛型类型T[]进行匹配并推断出T一定 String是。也就是说,可以调用

1
String middle ArrayAlg. getMiddle("John", "Q.", "Public");

几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解译错误报告。看一看下面这个示例:

1
double middle= ArrayAlg.getMiddle(3.14,1729,0);

错误消息会以晦涩的方式指出(不同的编译器给出的错误消息可能有所不同):解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器将会自动打包参数为1个 Double和2个 Integer对象,而后寻找这些类的共同超类型。事实上;找到2个这样的超类型: Number和 Comparable接口,其本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数都写为double值。

类型限定的变量

1
2
3
4
5
6
7
8
9
10
11
12
有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:
class ArrayAlg
{
public static <T> T min( a)//almost correct
{
if (a = null ||a.length==0) return null;
T smallest a[0];
for (int 1; i a. length; i++)
if (smallest. compareTo(a[i])> 0) smallest=a[i];
return smallest;
}
}

但是,这里有一个问题。请看一下min方法的代码内部。变量 smallest类型为T,这意味着它可以是任何一个类的对象。怎么才能确信T所属的类有 compareTo方法呢?
解决这个问题的方案是将T限制为实现了 Comparable接口(只含一个方法compareTo的标准接口)的类。可以通过对类型变量T设置限定( bound)实现这一点:

1
public static <T extends Comparable> T min(T[] a)...

实际上 Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译器产生的警告。
现在,泛型的min方法只能被实现了 Comparable接口的类(如String、 LocalDate等)的数组调用。由于 Rectangle类没有实现 Comparable接口,所以调用min将会产生一个编译错误。

读者或许会感到奇怪——在此为什么使用关键字 extends而不是 implements?毕竞, Comparable是一个接口。下面的记法

1
<T extends Boundinglype>

表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字 extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)。
一个类型变量或通配符可以有多个限定,例如:

1
T extends Comparable & Serializable

限定类型用“&”分隔,而逗号用来分隔类型变量。
在java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

接口

接口的概念

接口中的所有方法都自动是public方法。因此,在接口中声明方法时,不必提供关键字public。

接口的属性

接口不是类。具体来说,不能用new运算符实例化一个接口:

1
x =new Comparable(...);/erron

不过,尽管不能构造接口的对象,却能声明接口的变量:

1
Comparable x;//ok

接下来,如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口:

1
if(anObject instanceof Comparable){...}

与建立类的继承层次一样,也可以扩展接口。。这里允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。例如,假设有一个名为Moveable的接口:

1
2
3
4
public interface Moveable
{
void move(double x,double y);
}

然后,可以假设一个名为Powered的接口扩展了以上接口:

1
2
3
4
public interface Powered extends Moveable
{
double milePerGallon();
}

虽然在接口中不能包含实例字段,但是可以包含常量。

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?java的规则如下:

(1)超类优先。如果超类提供了一个具体方法,同名而且参数相同的默认方法会被忽略。

(2)接口冲突。如果一个接口提供了一个默认方法,另一个接口1提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。

回调

回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。

内部类

内部类是定义在另一个类中的类。使用的原因主要有两点:

(1)内部类可以对同一个包中的其他类隐藏。

(2)内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。

代理

利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。对于编写应用程序的程序员来说,这种情况很少见,所以如果对这种高级技术不感兴趣,可以跳过本节内容。不过,对于某些系统应用程序,代带来的灵活性可能十分重要。

何时使用代理

假设你想构造一个类的对象,这个类实现了一个多个接口,但是在编译时你可能并不知道这些接口到底是什么。这个问题确实有些难度。要想构造一个具体的类,只需要使用 newInstance方法或者使用反射找出构造器。但是,不能实例化接口。需要在运行的程序中定义一个新类。
为了解决这个问题,有些程序会生成代码,将这些代码放在一个文件中,调用编译器, 然后再加载得到的类文件。很自然地,这样做速度会比较慢,并且需要将编译器连同程序 一起部署。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现你指定的接口。具体地,代理类包含以下方法:

●指定接口所需要的全部方法。
●object类中的全部方法,例如, toString、 equals等。

不过,不能在运行时为这些方法定义新代码。实际上,必须提供一个调用处理器。调用处理器是实现了 InvocationHandler接口的类的对象。这个接口只有一个方法:

1
Object invoke(Object proxy, Method method, Object[] args)

无论何时调用代理对象的方法,调用处器的 invoke方法都会被调用,并向其传递Method对象和原调用的参数。之后调用处理必须确定如何处理这个调用。

创建代理对象

要想创建一个代理对象,需要使用 Proxy类 newProxyInstance方法。这个方法有三个参数:
●一个类加载器。作为java安全模型的一部分,可以对平台和应用类、 从因特网下载的类等使用不同的类加载器。
●一个 Class对象数组,每个元素对应需要实现的各个接口。
●一个调用处理器。
还有两个需要解决的问题。如何定义处理器?另外,对于得到的代理对象能够做些什么?当然,这两个问题的答案取决于我们想要用代理机制解决什么问题。使用代理可能出于很多目的,例如:

●将方法调用路由到远程服务器。
●在运行的程序中将用户界面事件与动作关联起来。
●为了调试,跟踪方法调用。

定义子类

关键词extends表明正在构造的新类派生于一个已存在的类。这个已存在的类1称为超类、基类或父类;新类称为子类、派生类或孩子类。

1
2
3
4
public class Manger extends Employee
{
addded methods and fields
}

覆盖方法

超类中的有些方法对子类并不一定适用。为此,需要提供一个新的方法来覆盖超类中的这个方法。

当我们希望调用超类中的方法,而不是当前类的这个方法,可以用特殊的关键词super来解决。

继承绝对不会删除任何字段或方法。

子类构造器

当子类的构造器不能访问父类的私有字段时,则必须通过一个构造器来初始化这些私有字段。可以利用特殊的super语法调用这个构造器。使用super调用构造器的语句必须是子类构造器的第一条语句。

如果子类构造器没有显示地调用超类的构造器,将自动调用超类的无参数构造器。如果超类没有无参数构造器,并且在子类的构造器中又没有显示地调用超类的其他构造器,java编译器就会报告一个错误。

多态

一个对象变量可以指示多种实际类型的现象被称为多态

在运行是能够自动地选择适当的方法,称为动态绑定

替换原则:程序中出现超类对象的任何地方都可以使用子类对象替换。

在java程序设计语言中,对象变量是多态的。

理解方法调用

(1)编译器查看对象的声明类型和方法名。

(2)接下来,编译器要确定方法调用中提供的参数类型。方法的名字和参数列表称为方法的签名

(3)如果是private方法,static方法,final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这称为静态绑定

(4)程序运行并且采用动态绑定调用方法时,虚拟机必须调用与x所引用对象的实际类型对应的那个方法。

警告:在覆盖一个方法时,子类方法不能低于超类方法的可见性。特别是,如果超类方法时public,子类方法必须也要声明为public。

阻止继承

有时候,我们可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表示这个类是final类。

类中的某个特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)。

如果一个方法没有被覆盖并且很短,编译器就能对它进行优化处理,这个过程称为内联

强制类型转换

只能在继承层次内进行强制类型转换。

在将超类强制转换成子类之前,应该使用instanceof进行检查。

抽象类

使用abstract关键字,则不需要实现该方法。

为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。

扩展抽象类可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也标记为抽象类;另一种做法是定义全部方法,这样一来,子类就不是抽象的了。

即使不含抽象方法,也可以将类声明为抽象类。

抽象类不能实例化。

受保护访问

子类也无法访问父类的私有对象。

当希望允许子类的访问父类的某个字段。为此,需要将这些类方法或字段声明为受保护。

在java中保护字段只能由同一个包的类访问。

object类型变量

在java中,只有基本类型不是对象,例如,数值、字符和布尔类型的值都不是对象。

相等测试与继承

java语言规范要求equals方法具有以下特性:

(1)自反性

对于任何非空引用想,x.equals(x)应该返回true。

(2)对称性

对于任何引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)返回true。

(3)传递性

对于任何引用x,y,z,如果x.equlas(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。

(4)一致性

如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

(5)对于任何非空引用x,x.equals(null)应该返回fasle

hasCode方法

散列码是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hasCode()与y.hasCode()基本上不会相同。

如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hasCode方法。

泛型数组列表

一旦能够确认数组列表的大小将保持恒定,不再发生变化,就可以调用trimToSize方法。这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间。垃圾回收器将回收多余的存储空间。

一旦削减了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会再向数组列表添加任何元素时再调用trimToSize。

参数数量可变的方法

1
2
System.out.printf("%d",n);
System.out.printf("%d %s",n,"widgets");

这两条语句都调用同一个方法,不过一个调用有两个参数,一个有三个。

printf方法是这样定义的:

1
2
3
4
public class PrintStream
{
public PrintStream printf(String fmt,Object...args){return format(fmt,args);}
}

继承的设计技巧

(1)将公共操作和字段放在超类中。

(2)不要使用受保护的字段。

(3)使用继承实现“is-a”关系。

(4)除非所有继承的方法都有意义,否则不要使用继承。

(5)在覆盖方法时,不要改变预期的行为。

(6)使用多态,而不要使用类型信息。

(7)不要滥用反射。

基本概念

:构造对象的模板和蓝图。

封装:有时称为数据隐藏。是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。

实例字段:对象中的数据。

方法:操作数据的过程。

继承:通过扩展一个类来建立另外一个类的过程称为继承。

知识点

构造器

构造器与类同名。在构造类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。

构造器与其他方法有一个重要不同。构造器总是结合new运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。

每个类可以有一个以上的构造器。

构造器可以有0个,1个或多个参数。

构造器没有返回值。

使用NULL值引用

一个对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有引用任何对象。

如果对null值应用一个方法,会产生一个NullPointerException异常。

这是一个很严重的错误,类似于“索引越界”异常。如果你的程序没有“捕获”异常,程序就会终止。正常情况下,程序并不捕获这些异常,而是依赖于程序员从一开始就不要带来异常。

隐式参数与显示参数

方法中的参数通常分为隐式参数与显示参数,隐式参数是出现在方法名前的类类型的对象。显示参数是位于方法名后面括号中的数值。

可用关键词this指示隐式参数,可以将实例字段与局部变量明显地区分开。

final实例字段

可以将实例字段定义为final。这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。

静态方法

静态方法是不在对象上执行的方法。可以认为静态方法是没有this参数的方法。

以下两种方法可以使用静态方法:

(1)方法不需要访问对象状态,因为它需要的所有参数都通过显示参数提供。

(2)方法只需要访问类的静态字段。

注:建议使用类名而不是对象来调用静态方法。

方法参数

按值调用:表示方法接受的是调用者提供的值。

按引用调用:表示方法接收的是调用者提供的变量地址。

方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。

java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个副本。

方法不能修改基本数据类型的参数。

方法可以改变对象参数的状态。

方法不能让一个对象参数引用一个新的对象。

重载

如果多个方法有相同的名字,不同的参数,便出现了重载。编译器必须挑出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。查找匹配的过程被称为重载解析。

构造器的具体处理步骤

(1)如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。

(2)否则,

a)所有数据字段初始化为其默认值(0,false,null)

b)按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。

(3)执行构造器主体代码。

类设计技巧

(1)一定要保证数据私有。

这是最重要的;绝对不要破坏封装性。有时候,可能需要编写一个访问器方法或更改器方法,但是最好还是保持实例字段的私有性。很多惨痛的教训告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug。

(2)一定要对数据进行初始化。

Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值。

(3)不要再类中使用过多的基本类型。

这个想法是要用其他的类替换使用多个相关的基本类型。

(4)不是所有的字段都需要单独的字段访问器和字段更改器。

(5)分解有过多职责的类。

这样说似乎有点含糊,究竟多少算是“过多”?每个人的看法都不同。但是,如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解。

(6)类名和方法名要能够体现它们的职责。

与变量应该有一个能够反映其含义的名字一样,类也应该如此。

(7)优先使用不可变的类。

LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的状态。类似 plusDays的方法并不是更改对象,而是返回状态已修改的新对象。
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。
因此,要尽可能让类是不可变的,

计算机系统

摘要

​ 本文遍历了hello.c在Linux下的生命周期,利用了Linux下的开发工具,通过对其进行预处理,编译,汇编,链接等过程的分布解读,来进行对各个过程Linux下的学习与理解。通过对其中的进程运行,内存管理,I/O管理等过程来探索深层次的Linux相关内容。重点在于将课本知识与实例进行结合,同时帮助复习,来将计算机系统的知识所贯通。

\关键词:****计算机系统;预处理;编译;汇编;链接;内存管理;Linux**

\目 录**

第1章 概述 \- 4 -**

1.1 Hello简介 - 4 -

1.2 环境与工具 - 4 -

1.3 中间结果 - 4 -

1.4 本章小结 - 4 -

第2章 预处理 \- 5 -**

2.1 预处理的概念与作用 - 5 -

2.2在Ubuntu下预处理的命令 - 5 -

2.3 Hello的预处理结果解析 - 5 -

2.4 本章小结 - 5 -

第3章 编译 \- 6 -**

3.1 编译的概念与作用 - 6 -

3.2 在Ubuntu下编译的命令 - 6 -

3.3 Hello的编译结果解析 - 6 -

3.4 本章小结 - 6 -

第4章 汇编 \- 7 -**

4.1 汇编的概念与作用 - 7 -

4.2 在Ubuntu下汇编的命令 - 7 -

4.3 可重定位目标elf格式 - 7 -

4.4 Hello.o的结果解析 - 7 -

4.5 本章小结 - 7 -

第5章 链接 \- 8 -**

5.1 链接的概念与作用 - 8 -

5.2 在Ubuntu下链接的命令 - 8 -

5.3 可执行目标文件hello的格式 - 8 -

5.4 hello的虚拟地址空间 - 8 -

5.5 链接的重定位过程分析 - 8 -

5.6 hello的执行流程 - 8 -

5.7 Hello的动态链接分析 - 8 -

5.8 本章小结 - 9 -

第6章 hello进程管理 \- 10 -**

6.1 进程的概念与作用 - 10 -

6.2 简述壳Shell-bash的作用与处理流程 - 10 -

6.3 Hello的fork进程创建过程 - 10 -

6.4 Hello的execve过程 - 10 -

6.5 Hello的进程执行 - 10 -

6.6 hello的异常与信号处理 - 10 -

6.7本章小结 - 10 -

第7章 hello的存储管理 \- 11 -**

7.1 hello的存储器地址空间 - 11 -

7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -

7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -

7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -

7.5 三级Cache支持下的物理内存访问 - 11 -

7.6 hello进程fork时的内存映射 - 11 -

7.7 hello进程execve时的内存映射 - 11 -

7.8 缺页故障与缺页中断处理 - 11 -

7.9动态存储分配管理 - 11 -

7.10本章小结 - 12 -

第8章 hello的IO管理 \- 13 -**

8.1 Linux的IO设备管理方法 - 13 -

8.2 简述Unix IO接口及其函数 - 13 -

8.3 printf的实现分析 - 13 -

8.4 getchar的实现分析 - 13 -

8.5本章小结 - 13 -

结论 \- 14 -**

附件 \- 15 -**

参考文献 \- 16 -**

第一章 概述

1.1 Hello简介

P2P:From Program to Process

从源文件到目标文件的转换是由编译器驱动程序完成的。

在这里,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行的目标文件hello。这个编译可以分为四部分进行,执行这四个阶段的程序(预处理器,编译器,汇编器,链接器)一起构成了编译系统。

Hello.c->(cpp)hello.i->(ccl)hello.s->(as)hello.o->(ld)hello

预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。比如hello.c中第一行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本。结果就得到了另一个C程序,通常以.i作为文件扩展名。

编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。它包含一个汇编语言程序。

汇编阶段:接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件,它包含的17个字节是函数main的指令编码。

链接阶段:请注意,hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。

O2O: From Zero-0 to Zero-0

shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码段、数据、bss以及栈区域创建新的区域结构,然后映射虚拟内存,设置程序计数器,使之指向代码区域的入口点,进入程序入口后程序开始载入物理内存,而后进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。hello执行完成后shell父进程会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成O2O的过程。

1.2 环境与工具

1.2.1 硬件环境

系统型号:81T0

BIOS:BHCN26WW

处理器:i5—9300H

内存:8192MB RAM

1.2.2 软件环境

操作系统:win10家庭中文版64位

VMware Workstation Pro

Ubuntu20.04

1.2.3 开发工具

CodeBlocks

gcc

VsCode

1.3 中间结果

hello.i预处理后的文件 gcc -E hello.c -o hello.i

hello.s编译后的文件 gcc -S hello.i -o hello.s

hello.o汇编后的可重定位目标程序 gcc -c hello.s -o hello.o

hello链接后的可执行目标文件 gcc -o hello hello.o

hello_objd.txt:链接后的hello可执行文件经过反汇编生成的代码

objdump -d hello.o
ELFout.txt:链接后的hello可执行文件经过readelf读取的ELF信息

readelf -a hello.o >hello.elf

1.4 本章小结

本章对hello做了一个总体的概述,了解了P2P与020,简述了硬件与软件环境,并了解了中间产物以及其作用。

\第2章** \预处理**

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,读取头文件stdio.h的内容,并把它直接插入程序文本中。

作用:

(1)预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。比如hello.c中第一行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本。结果就得到了另一个C程序,通常以.i作为文件扩展名。

(2)用实际值替换用#define定义的字符串

(3)根据#if后面的条件决定需要编译的代码

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

img

img

2.3 Hello的预处理结果解析

img

img

img

img

img

img

img

img

img

img

img

img

2.4 本章小结

​ 本章了解了预处理的概念及作用,以及在linux系统下的指令,同时解析了预处理的文本内容,让我对预处理有了更一步的了解和较为深刻的认识,更好的理解了计算机系统。

\第3章** \编译**

3.1 编译的概念与作用

概念:编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

作用:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含函数main的定义,其中每条语句都以一种文本格式描述了一条低级语言指令。编译主要作用除了是将文本文件hello.i翻译成文本文件hello.s之外,还在出现语法错误时给出提示信息。执行过程主要从其中几个阶段进行分析:

(1)词法分析:词法分析是使用一种叫做lex的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。

(2)语法分析:语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。用于语法分析也有一个现成的工具,叫做:yacc。

(3)语义分析:语法分析完成了对表达式语法层面的分析,但是它不了解这个语句是否真正有意义。有的语句在语法上是合法的,但是却是没有实际的意义,比如说两个指针的做乘法运算,这个时候就需要进行语义分析,但是编译器能分析的语义也只有静态语义。

(4)中间代码生成:我们的代码是可以进行优化的,对于一些在编译期间就能确定的值,是会将它进行优化的,比如说上边例子中的 2+6,在编译期间就可以确定他的值为8了,但是直接在语法上进行优化的话比较困难,这时优化器会先将语法树转成中间代码。中间代码一般与目标机器和运行环境无关。(不包含数据的尺寸、变量地址和寄存器的名字等)。中间代码在不同的编译器中有着不同的形式,比较常见的有三地址码和P-代码。

(5)目标代码生成与优化:代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

img

img

3.3 Hello的编译结果解析

img

img

img

\3.3.1 数据**

(1)全局变量

int sleeptime=2;

全局变量sleeptime被设置为int,由于int与long大小相同,所以被转换为long,数值大小为2,被赋予4个字节内存,对齐方式设置为4,储存在.data中。

(2)局部变量

int i;

通常储存在栈或者是寄存器中,在这个程序中,i被存放在栈中

而argc作为传入main的参数同样储存在栈中

(3)字符串

代码中出现的字符串,存放在.roarta中

\3.3.2 赋值**

对全局变量的赋初值直接在汇编后代码。

img

对循环变量i的赋值,局部变量i保存在栈中,存放在-4(%rbp)中,对其赋初值为0。

img

\3.3.3 类型转换**

将全局变量的int转换为long类型。都占据4个字节。

img

img

\3.3.4 算数操作**

当i的值小于8时,将i的值每次循环+1。

img

\3.3.5 关系操作**

判断argc是否等于3

img

img

通过cmpl来判断,argc存放在-20(%rbp)中

通过je跳转

判断循环变量i是否小于8

img

img

通过cmpl来判断,i存放在-4(%rbp)中

通过jle跳转

\3.3.6 数组/指针/结构操作**

在代码中对数组的引用

img

分别存放argv[0]与argv[1]

\3.3.7 控制转移**

img

赋初值给i,开始进行循环

img

判断是否满足循环条件

img

判断argc是否等于3

\3.3.8 函数操作**

img

调用printf与sleep

img

主函数

img

return

3.4 本章小结

​ 通过对文本文件hello.c的学习,再次强调了汇编语言的重要性。

​ 编译器将高级语言编译成汇编语言,在以上的分析过程中,详细的分析了编译器是怎么处理C语言的各个数据类型以及各类操作的,按照不同的数据类型和操作格式,解释了hello.c文件与hello.s文件间的映射关系。通过对汇编语言的分析,加深了对各种操作的掌握。

\第4章** \汇编**

4.1 汇编的概念与作用

概念与作用:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件,它包含的17个字节是函数main的指令编码。

4.2 在Ubuntu下汇编的命令

img

img

4.3 可重定位目标elf格式

img

img

img

img

img

ELF头以一个十六字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,机器类型,节头部标中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每一个节都有一个固定大小的条目。

.text:已编译程序的机器代码

.rodata:只读数据

.data:已初始化的全局和静态C变量

.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量

.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息

.rel.text:一个.text节中位置的列表

.rel.data:被模块引用或定义的所有全局变量的重定位信息

.debug:一个调试符号表

.line:原始C源程序中的行号和.text节中机器指令之间的映射

.strtab:一个字符串表

4.4 Hello.o的结果解析

img

img

机器语言的构成:
机器的指令指令是CPU能直接识别并执行的指令,它的表现形式是二进制编码。
通常由操作码和操作数两部分组成,操作码指出该指令所要完成的操作,即指令的功能,操作数指出参与运算的对象,以及运算结果所存放的位置等。

汇编前后代码区别:

经过汇编之后,hello.o文件得到了ELF格式信息,重定位信息,所以发生了以下变化:

(1)所有机器数都由10进制变成了便于机器操作的16进制

(2)在函数调用方面,之前是直接调用,在汇编之后,变成了通过程序计数器(PC)的变化来转移

(3)跳转方面,也由直接跳转变成了通过程序计数器来转变

(4)对于全局变量的引用,某些全局变量因为他们的地址需要在运行后才能确定,所以访问需要重定位;在汇编后的代码,所以在汇编后,这些操作数都被置于零,添加重定位条目

4.5 本章小结

​ 汇编阶段,通过汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在hello.o中。

​ 通过实验,了解了汇编前后代码的区别以及ELF表的相关内容,对汇编的掌握又提升了一部分。

\第****5****章* *链接**

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

5.2 在Ubuntu下链接的命令

gcc -o hello hello.o

img

img

5.3 可执行目标文件hello的格式

readelf -a hello > hello_out.elf

ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

img

img

img

img

img

重定位节

img

img

img

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

img

edb使用方法

img

可以从Data Dump中查看虚拟空间的地址,程序的虚拟空间地址为

img

首先查看hello_out.elf中的程序头部分,如下图:

img

此处PHDR表示该段具有读/执行权限,表示自身所在的程序头部表在内存中的位置为内存起始位置0x400000偏移0x40字节处

img

此处INTERP位于偏移0x318处,该段具有读权限,记录了程序所用ELF解析器

img

LOAD段位于开始处,有读/执行访问权限,其中包括ELF头、程序头部表以及.init、.text、.rodata字节

img

DYNAMIC在偏移0x2da8处,则与之对应

img

NOTE在偏移0x338处,则与之对应

img

5.5 链接的重定位过程分析

1.对于包含的函数:在hello.o反汇编生成的代码中,只有main函数,而在链接后生成的反汇编代码中,出现了调用的其他函数,比如printf,puts,getchar,exit,sleep等

2.对于函数调用和跳转:在hello.o反汇编生成的代码中,跳转都是一个偏移量,并在后面加上重定位条目,而在链接后生成的反汇编代码中,函数调用和跳转拥有了实际上的虚拟地址和函数,链接前的函数调用,调用地址为空,添加重定位条目,链接后的函数调用,拥有了实际的虚拟地址和函数名,不再需要重定位条目

3.增加了节:在hello.o反汇编生成的代码中,只有.text一个节,而在链接后生成的反汇编代码中,又添加了.init节和.fini节

4.地址引用和全局变量的引用:在hello.o反汇编生成的代码中,对全局变量的引用是通过重定位符号来描述的,而在链接后生成的反汇编代码中,随着链接的完成,有些需要在运行时确定的地址得到确定的变量被确定了虚拟内存地址

5.6 hello的执行流程

0000000000401000 <_init>:

0000000000401030 puts@plt:

0000000000401040 printf@plt:

0000000000401050 getchar@plt:

0000000000401060 exit@plt:

0000000000401070 sleep@plt:

0000000000401080 :

0000000000401110 <_start>:

0000000000401140 <_dl_relocate_static_pie>:

0000000000401150 <_libc_csu_init>

00000000004011b0 <_libc_csu_fini>

00000000004011b4 <_fini>:

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

根据hello ELF可知,.got起始位置为0x3f98

img

在调用dl_init之前其后16字节均为0

img

调用_start之后发生改变,其后的两个8个字节分别改变,其中GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下:

img

可以看出其已经动态链接,GOT条目已经改变。

5.8 本章小结

​ 链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在进行本章操作后,对于链接过程及其中间变化有了新的了解,对于这部分知识也加深了掌握。

\第****6****章* *hello进程管理**

6.1 进程的概念与作用

进程的经典定义是一个执行中程序的实例。系统中的每个程序都运行在某个进程上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

进程的作用是给在在运行一个程序时,得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

6.2 简述壳Shell-bash的作用与处理流程

shell是一种命令解释器,指为操作系统的使用者提供操作界面,它接收用户命令,然后调用相应的应用程序,交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令。

Shell-bash的处理流程

(1)读取用户由键盘输入的命令行,对其进行语法检查,如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息

(2)如果是内部命令直接执行

(3)若不是内部命令,则是可执行程序,分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式,传给Linux内核。

(4)终端进程调用fork( )建立一个子进程。

(5)终端进程本身调用wait4()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。

(6).如果命令末尾有&,则终端进程不用执行系统调用wait4(),立即发提示符,让用户输入下一条命令;否则终端进程会一直等待,当子进程完成工作后,向父进程报告,此时中断进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。

6.3 Hello的fork进程创建过程

当shell读取到执行hello程序时候,shell会分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式,传给Linux内核。

然后终端进程调用fork( )建立一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

最后终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。

fork具有以下特性:

(1)调用一次,返回两次。

(2)并发执行。

(3)相同但是独立的地址空间。

(4)共享文件。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。

execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve 才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。

在execve加载了hello之后,调用启动代码libc_start_main。启动代码设置栈,并将控制传递给新程序的主函数,该主函数具有如下形式的原型

int main(int argc,char argv ,char envp)

execve过程执行以下内容:

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

2.映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些

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

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

5.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

hello是一个进程,在执行的时得到一个抽象,就好像hello是系统中当前运行的唯一的程序一样。hello拥有一个独立的逻辑控制流,就像程序独占地使用处理器,同时hello拥有一个私有的地址空间,就像我们的程序独占地使用内存系统。

逻辑控制流和时间片

内核为每个进程(例如hello)维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。

上下文切换的过程

(1)保存当前进程的上下文

(2)恢复某个先前被抢占的进程被保存的上下文

(3)将控制传递给这个新恢复的进程

一个逻辑流的执行在时间上与另一个流重叠,称为并发流;这两个流被称为并发地运行。

进程时间片

多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

用户态与核心态转换

当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。般而言, 即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

异常

1.中断:当输入Ctrl-Z,进程收到信号SIGSTP,为暂停运行,直到收到SIGCONT

2.终止:当输如Ctrl-C,信号SIGINT,进程终止

信号:

信号SIGSTP:通过Ctrl-Z,为暂停运行直到收到下一个SIGCONT

信号SIGINT:通过Ctrl-C,进程终止

(1)直接运行,不再执行任何操作:

img

程序会正常运行直到结束,程序结束后进程被回收

(2) 运行后输入Ctrl-C

img

shell收到来自键盘输入的终止信号,经过信号处理程序后终止hello进程,并回收进程空间

(3)运行后输入Ctrl-Z

img

shell收到来自键盘输入的SIGSTP,暂停hello进程运行直到收到下一个SIGCONT

(4)进程暂停后输入jobs

img

会打印当前shell执行的进程的pid,状态,和名称

(5)进程暂停后输入ps

img

ps命令会打印出当前系统的的进程的各种信息

(6)进程暂停后输入pstree

img

img

img

输入pstree指令后会查看进程树之间的关系,即哪个进程是父进程,哪个是子进程,可以清楚的看出来是谁创建了谁。

(7)进程暂停后输入fg

img

fg指令将本来在后台挂起的hello进程恢复执行

(8)进程暂停后输入kill指令

首先在进程暂停后利用ps获取hello的pid

img

然后输入kill -9 -pid来发送信号给hello进程使其无条件终止

(9)进程运行过程中不停乱按键盘

img

在运行过程中输入乱码发现输入不会影响进程的运行,当按到回车键时,之前输入的字符会被读入缓冲区等待getchar处理,回车后再输入的字符会被当做是输入shell的命令。

6.7本章小结

​ 本章介绍了hello程序在shell中运行形式,并通过此方式延申至对于进程以及unix环境下终端指令的实验,使得对进程工作原理更加熟悉了解。明白了有关进程创建,管理运行以及终止/上下文切换的各种知识。对于各种指令有了直观的体验,增加了对于fork与execve的理解。对于异常控制流与虚拟内存部分的内容有了更深的掌握。

\第****7****章* *hello的存储管理**

7.1 hello的存储器地址空间

逻辑地址:是指由程序产生的与段相关的偏移地址部分。用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置。

线性地址:如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间,为了简化讨论,我们总是假设使用的是线性地址空间。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:在一个带虚拟内存的系统.中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都对应具体的物理地址。

段式管理:逻辑地址—>线性地址==虚拟地址

页式管理:虚拟地址—>物理地址。

img

在hello的反汇编文件中可以看到,很多都是用偏移地址表示的。

7.2 Intel逻辑地址到线性地址的变换-段式管理

段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。

在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。

在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。

段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。

程序通过分段划分为多个模块,如代码段、数据段、共享段:

Linux x86-64内存映像:

–可以分别编写和编译

–可以针对不同类型的段采取不同的保护

–可以按段为单位来进行共享,包括通过动态链接进行代码共享

这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。

7.3 Hello的线性地址到物理地址的变换-页式管理

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。

在任意时刻,虚拟页面的集合都分为三个不相交的子集:

(1)未分配的:VM系统还未分配的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。

(2)缓存的:当前已缓存在物理内存中的已分配页。

(3)未缓存的:未缓存在物理内存中的已分配页。

形式上说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射。

当页面命中时,CPU硬件执行的操作为:

(1)处理器生成一个虚拟地址,并把它传送给MMU。

(2)MMU生成PTE地址,并从高速缓存/主存请求得到它。

(3)高速缓存/主存向MMU返回PTE。

(4)MMU构造物理地址,并把它传送给高速缓存/主存。

(5)高速缓存/主存返回所请求的数据字给处理器。

处理缺页时要求硬件和操作系统内核协作完成:

(1)一到三步同上。

(4)PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页处理程序。

(5)缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。

(6)缺页处理程序页面调入新的页面,并更新内存中的PTE。

(7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。

7.4 TLB与四级页表支持下的VA到PA的变换

TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2^t个组,那么TLB索引是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余位组成的。

(1)CPU产生一个虚拟地址。

(2)MMU从TLB中取出相应的PTE

(3)MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存中。

(4)高速缓存/主存将所请求的数据字返回给CPU。

7.5 三级Cache支持下的物理内存访问

一级页表中的每个PTE负责映射虚拟地址空间中一个4MB的片,这里每一片都是由1024个连续的页面组成的。

如果片i中的每个页面都未被分配,那么一级PTEi就为空。然而,如果在片i中至少有一个页是分配了的,那么一级PTEi就指向一个二级页表的基址。

这种方法从两个方面减少了内存要求。第一,如果一级页表中第一个PTE是空的,那么对应的二级页表根本不会存在。这代表着一种巨大的潜在节约,因为对于一个典型的程序,4GB的虚拟地址空间的大部分都会是未分配的。第二,只有一级页表才需要总是在主存中;虚拟内存系统可以在需要时创建,页面调入或调出二级页表,这就减少了主存的压力,只有最经常用的才会在主存中。

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

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

(2)映射私有区域。为新程序的代码,数据,bss和栈区域创建新的区域结构。所有这些区域都是私有的,写时复制。

(3)映射共享区域。如果hello.out程序与共享对象链接,那么这些对象都是动态连接到这个程序的,然后在映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

(1)处理器生成一个虚拟地址,并把它传送给MMU。

(2)MMU生成PTE地址,并从高速缓存/主存请求得到它。

(3)高速缓存/主存向MMU返回PTE。

(4)PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页处理程序。

(5)缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。

(6)缺页处理程序页面调入新的页面,并更新内存中的PTE。

(7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。

缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序。

缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果指令不合法,缺页处理程序就触发一个段错误、终止进程。

缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。

缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。无论是否被修改,牺牲页的页表条目均会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。

7.9动态存储分配管理

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

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

2.隐式分配器(implicitallocator),另–方面,要求分配器检测-一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collecor)而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。

例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

对于显式分配器必须在一些相当严格的约束条件下工作:

1.处理任意请求序列。一个应用可以有任意的分配请求和释放请求序列,只要满足约束条件:每个释放请求必须对应于一个当前已分配块,这个块是由一个以前的分配请求获得的。因此,分配器不可以假设分配和释放请求的顺序。例如,分配器不能假设所有的分配请求都有相匹配的释放请求,或者有相匹配的分配和空闲请求是嵌套的。

2.立即响应请求。分配器必须立即响应分配请求。因此,不允许分配器为了提高性能重新排列或者缓冲请求。

3.只使用堆。为了使分配器是可扩展的,分配器使用的任何非标量数据结构都必须保存在堆里。

4.对齐块(对齐要求)。分配器必须对齐块,使得它们可以保存任何类型的数据对象。

5.不修改已分配的块。分配器只能操作或者改变空闲块。特别是,-旦块被分配了,就不允许修改或者移动它了。因此,诸如压缩已分配块这样的技术是不允许使用的。

7.10本章小结

本章从Linux存储器的地址空间起,阐述了Intel的段式管理和页式管理机制,以及TLB与多级页表支持下的VA到PA的转换,同时对cache支持下的物理内存访问做了说明。针对内存映射及管理,简述了hello的fork和execve内存映射,了解了缺页故障与缺页中断处理程序,对动态分配管理做了系统阐述。通过本章学习,对于虚拟内存部分的大部分内容,如虚拟地址,catch命中等进行了巩固,并加深了对其的了解。

\ ****第****8****章* *hello的IO管理**

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m个字节的序列:B0,B1,B2,……B(m-1)。所有的I/O设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需要记住这个描述符。

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出和标准错误。

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

读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能够检测到这个条件。

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

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

O_RDONLY:只读。

O_WRONLY:只写。

O_RDWR:可读可写。

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

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

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

首先查看printf函数的函数体

img

va_list的定义是:typedef char * va_list,因此通过调用va_start函数,获得的arg为第一个参数的地址。

vsprintf的作用是格式化。接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,例如hello中:

printf(“Hello %s %s\n”,argv[1],argv[2]);

命令行参数为./hello 1173710217 hpy,则对应格式化后的字符串为:Hello 1173710217 hpy\n,并且i为返回的字符串长度

接下来是write函数:

img

根据代码可知内核向寄存器传递几个参数后,中断调用syscall函数。对应ebx打印输出的buf数组中第一个元素的地址,ecx是要打印输出的个数。查看syscall函数体:

img

在syscall函数中字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现printf格式化输出。

8.4 getchar的实现分析

getchar源代码为:

img

异步异常-键盘中断的处理:键盘中断处理是底层的硬件异常,当用户按下键盘时,内核会调用异常键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar函数read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。实现读取一个字符的功能。

8.5本章小结

本章系统的了解了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制。通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。通过本章的学习,对于程序间的交互和通信有了进一步的理解。

\结论**

让我们来一起回顾一下hello的一生:

(1)hello.c经过预处理器处理,变成了hello.i。

(2)hello.i经过编译器,变成了hello.s。

(3)hello.s经过汇编器,被翻译成了机器指令,打包为可重定位目标文件hello.o。

(4)hello.o经过链接终于变成了可执行文件hello。

(5)在Linux下键入./hello likaijie 1190201617运行hello,内核为hello fork出新进程,并在新进程中execve hello程序。

(6)execve 通过加载器将hello中的代码和数据从磁盘复制到内存,为其创建虚拟内存映像,加载器在程序头部表的引导下将hello的片复制到代码段和数据段,执行_start函数。

(7)MMU通过页表将虚拟地址映射到对应的物理地址完成访存。

(8)内核通过GOT和PLT协同工作完成共享库函数的调用。

(9)hello调用函数,内核通过动态内存分配器为其分配内存。

(10)内核通过调度完成hello和其他所有进程的上下文切换,成功运行hello。

(11)shell父进程回收hello,内核删除hello进程的所有痕迹。

至此,hello走完了它的一生。

回顾hello短暂的一生,却又并不短暂,每一步都是人类智慧的结晶,为了这短短的几步,人类走了许久,自1946年第一台电子计算机问世以来,就在不断探索,计算机技术在元件器件、硬件系统结构、软件系统、应用等方面,均有惊人进步。从两个足球场大的计算机到如今我们面前的小小的笔记本,令人叹服。

回顾计算机系统漫游,学习了信息的表示和处理,程序的机器级表示,处理器体系结构,优化程序性能,存储器层次结构,了解了链接,异常控制流与虚拟内存,见识了程序间的交互和通信。都展现了计算机迷人的魅力。

\ ****附件**

列出所有的中间产物的文件名,并予以说明起作用。

(1)hello.c:源代码

(2)hello.i:预处理生成的文本文件

(3)hello.s:编译翻译成的文本文件

(4)hello.o:汇编器翻译成的机器指令打包成的可重定位二进制文件

(5)hello:链接后的可执行文件

(6)hello.elf:hello.o的ELF格式

(7)helloobj.txt:hello.o生成的反汇编文件

(8)hello_out.elf:hello生成的ELF格式

\参考文献**

\为完成本次大作业你翻阅的书籍与网站等**

[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7]Computer System A Programmer’s Perspective Third Edition,2015,Randal E. Bryant,

David R.O’Hallaron

[8]读计算机系统有感 2011.6.10 马旭东

[9]深入计算机系统之道 2006.9.1 刘江

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

异常概述

在程序中,错误可能产生于程序没有预料到的各种情况,或者是超出了程序员可控范围的环境因素,如用户的坏数据,试图打开一个不存在的文件夹等。在java中这种在程序运行时可能出现的一些错误称为异常。异常是一个在程序执行期间发生的事件,它中断了正在执行的程序的正常的指令流。

处理程序异常错误

​ 在java中,如果某个方法抛出异常,既可以在当前方法中捕获异常,也可以将异常向上抛出,由方法调用者来处理。

错误

1
2
3
4
         String s="xhy";
System.out.println(s+"年龄是:");
int age=Integer.parseInt("20L");
System.out.println(age);

​ 在本段代码中,实现将非字符型数值转换为int型,程序会报出异常。

image-20210530090934105

​ 可以看出,本实例报出的是NumberFormatException(字符串转换为数字异常)。提示信息“xhy年龄是”已经输出,而变量age没有输出,可知程序在执行类型代码转换时已经终止。

捕捉异常

​ java语言的异常捕获结构是由try,catch和finally三部分组成,try语句存放的是可能发生异常的java语句;catch程序块在try之后,用来激发被捕获的异常;finally语句块是异常处理结构的最后执行部分,无论try语句中的代码块如何退出,都将执行finally语句块。

​ 语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
try{
//程序代码块
}
catch(Exceptiontype1 e){
//对Exceptiontype1的处理
}
catch(Exceptiontype2 e){
//对Exceptiontype2的处理
}
finally{
//finally程序块
}

利用try -catch语句对刚刚代码行进行修改

1
2
3
4
5
6
7
8
9
try {
String s="xhy";
System.out.println(s+"年龄是:");
int age=Integer.parseInt("20L");
System.out.println(age);
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("over");

image-20210530091539131

​ 可以看出,程序仍然输出最后的提示信息,没有因为异常而终止。当try代码块中的语句发生异常时,程序就会跳转到catch中执行,执行完catch中的代码块后,将继续执行catch后的代码块。由此可知,java的异常处理是结构化的,不会因为一个异常而影响整个代码的执行。

注意. Exception是try代码块传递给catch代码块的变量类型,e是变量名。
异常捕捉函数

这里介绍三个异常捕捉函数:

getMessage()函数:输出错误性质。

image-20210530011148818

toString()函数:给出异常的类型与性质。

image-20210530091428578

printStackTrace()函数:

见上图。

注意:前两个函数需要打印,而第三个不需打印。

finally语句块

完整的异常处理语句一定要包含finally语句,但以下四种情况finally语句不会被执行。

(1)在finally语句块中发生异常。

(2)在前面的代码中使用了System.exit(0)退出了程序。

(3)程序所在的线程死亡。

(4)CPU关闭。

自定义异常

用户只需要继承Exception类即可自定义异常类。

在程序中使用自定义异常类,大致可分为以下几个步骤:

(1)创建自定义异常类。

(2)在方法中通过throw关键字抛出异常类。

(3)若在当前抛出异常的方法中处理异常,可以使用try-catch语句块捕获并处理,否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。

(4)在出现异常方法的调用者中捕获并处理异常。

创建自定义异常,在项目中创建类MyException,该类继承Exception。
1
2
3
4
5
public class MyException extends Exception {
public MyException(String ErrorMessage) {
super(ErrorMessage);
}
}
在项目中创建类Tran,该类中创建一个带有int型参数的方法avg(),该方法用来检测参数中是否有数小于0,如果存在,则通过throw关键字抛出MyException异常对象,并在main函数中捕捉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Tran {
static int avg(int number1,int number2)throws MyException{
if(number1<0||number2<0) {
throw new MyException("不可使用负数");
}
return(number1+number2)/2;
}
public static void main(String[]args) {
try {
int result=avg(102,-2);
System.out.println(result);
}catch(MyException e) {
System.out.println(e);
}
}
}

运行结果如图

image-20210530145801591

运行时异常

Java类库的每个包都定义了异常类,所有这些类都是Throwable类的子类。Throwable类派生了两个子类,分别是Exception和Error类。Error类及其子类用来描述Java运行系统中的内部错误以及资源耗尽的错误,这类错误一般比较严重。Exception类称为非致命类,可以通过捕捉处理使程序继续执行。

异常的使用原则

(1)在当前方法声明中使用try-catch语句捕获异常。

(2)一个方法被覆盖时,覆盖它的方法必须抛出相同的异常或异常的子类。

(3)如果父类抛出多个异常,则覆盖方法必须抛出那些异常的一个子集,不能抛出新异常。

建议:不要将异常抛出,应该编写异常处理语句。

人各有命

  • 第一篇博客
1
2
3
4
5
#include <stdio.h>
int main()
{
return 0;
}