0%

文件

Linux中文件的概念并没有那么纯粹,不单单指的是磁盘上的文件。Linux中只要是字节序列构成的载体,都可以是文件,比如I/O设备、socket套接字等。总的来说大致分为以下几类:

  • 普通文件。即磁盘上的文件
  • 目录文件。即目录
  • 链接文件。Windows上称为快捷方式,但是在Linux中链接文件还分为软链接(符号链接)与硬链接
  • 设备文件。Linux中将设备看作文件来进行管理,分为块设备与字符设备。
  • socket文件。用于网络通信
  • 管道文件。用于IPC

文件系统结构

在Linux中,文件系统并不是像Windows中的那样分为一个一个盘,而是以挂载到别的文件系统的形式存在的。这种结构可以简单地看成树型结构,根结点则是根文件系统。

文件系统

Linux可以支持很多文件系统,这些文件系统之间的数据也可以是互通的。Linux使用了VFS这一结构来管理文件系统。

VFS即虚拟文件系统,在各种具体的文件系统之上建立一个抽象层,屏蔽了不同文件系统之间的差异。这种抽象层其实也像POSIX标准一样,为了屏蔽底层的细节而设计的。

VFS

VFS主要有如下的四个对象类型:

  • 超级块(struct super_block)。超级块代表一个已经安装的文件系统,存储着该文件系统的有关信息,比如文件系统的类型、大小、状态等。
  • 索引结点(struct inode)。索引结点对象表示存储设备上的一个实际的物理文件,存储该文件的有关信息,比如权限、大小以及创建时间等。
  • 目录项(struct dentry)。目录项描述了文件系统的层次结构,不管是目录还是普通的文件,都是一个目录项对象。
  • 文件(struct file)。文件对象代表已经被进程打开的文件,主要用于建立进程和文件之间的对应关系。

值得注意的是,目录项并不是存在于磁盘中的信息,而是存在于内存当中,相当于磁盘的缓存。

文件

文件对象描述已经打开的文件,对于进程来说,能够直接进行处理的是文件,而不是超级块之类的。对于每个inode,可以对应多个文件对象,而一个文件对象只能对应一个inode。文件对象包含了对文件的相关操作struct file_operations,包括打开、关闭、读写等等。

与文件系统相关的结构

超级块中包含了一些其他的文件系统有关的结构体,例如struct file_system_type,表示文件系统类型。对于ext3文件系统来说定义如下:

1
2
3
4
5
6
7
static struct file_system_type ext3_fs_type = {
.owner = THIS_MODULE,
.name = "ext3",
.get_sb = ext3_get_sb,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};

struct vfsmount则包含了文件系统挂载的信息,包括挂载点、根结点等。

与进程相关的结构

每个进程都有自己的根目录与当前工作目录,例如在shell中,可以使用pwd(print working directory)命令来打印当前工作目录。除此之外,进程还需要记录自己打开的文件,进程打开的文件用struct files_struct来表示,里面就包括了struct file数组,记录已经打开的文件信息。

inotify机制

简单来说,inotify机制是用来监听文件的变化的,类似与设备发送中断来表示设备状态发生了变化,操作系统也会发送信号来表示文件发生了变化,而这一点在Linux是通过inotify机制来实现的。

每个inode结点上都有一个inotify_watches字段,指向了该inode对象的监控列表,一个watch实例是一个struct inotify_watch结构的实例,代表了对该inode的监控请求,其中就包含了事件处理函数。

inotify的实现原理是,在各个文件操作函数的hook通过inode的成员inotify_watches找到监控列表,然后再通过监控列表找到事件处理函数,然后再进行处理。

前言

大家都知道,如果二叉查找树不平衡的话,查找的时间复杂度就会降为O(n),也就是说二叉查找树退化成链表了。因此,需要让二叉查找树维持平衡,这样就算在最坏的情况下,基本的操作的时间复杂度也会降低到O(logn)。

红黑树的几个性质

红黑树,结点上有一个存储位用来表示结点是红色的还是黑色的,因此叫红黑树。红黑树可以确保从根节点到叶结点的任意两条路径的长度不会相差两倍,因此红黑树是相对平衡的。

一颗二叉查找树需要满足一下的性质,才能称为红黑树:

  • 每个结点要么红色,要么是黑色;
  • 根节点为黑色的;
  • 每个叶结点(NIL)是黑色的;
  • 红结点的子节点只能是黑色,也就是说不能够出现两个红结点相邻的情况;
  • 对于每个结点,从它出发到任意子结点的路径,黑结点的数量是相同的(称为黑高度相等)。

采取《算法导论》一书中对于红黑树的编写方法,设置一个哨兵对象NIL结点来作为所有结点的空结点,即对于没有子结点的结点,将它们的子结点指向NIL结点:

1
2
3
    A                       A
| | -> | |
NULL NULL NIL NIL

旋转

二叉树的旋转是一种比较常见的操作,包括了左旋、右旋与双旋。

左旋是对结点进行逆时针旋转,让原本结点的右结点成为新的父结点,而原来的父结点成为新的父结点的左结点。右旋与左旋的情况相反,右旋为顺时针旋转,原来的左结点成为新的父结点,而原来的父结点成为新的父结点的右结点。

双旋包括了左旋后右旋与右旋后左旋。

左旋的为代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void leftRotate(TreeNode node) {
// 右结点成为新的父节点
TreeNdoe newParent = node.right;

// 原右结点的左子结点成为原父节点的右结点
node.right = newParent.left;
node.right.parent = node;

// 右结点成为父节点,原父结点成为新父节点的左子结点
newParent.left = node;
newParent.parent = node.parent;
node.parent = newParent;

if (newParent.parent = nil) {
// 根结点
treeRoot = newParent;
} else if (newParent.parent.left == node) {
// 设置原父结点的父结点的左右子结点为新的父结点
newParent.parent.left = newParent;
} else {
newParent.parent.right = newParent;
}
}

插入

想红黑树中插入一个新的结点的时间复杂度为O(logn),新的结点插入的方法与二叉查找树的插入方法基本相同,只不过多了几个步骤来保证红黑树的性质不会被破坏。在插入新结点之后,将新结点着色为红色,然后调用修复修复函数来对结点重新着色与旋转,保证红黑树的性质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void insert(TreeNdoe newNode) {
TreeNode currRoot = treeRoot;
TreeNode newParent = nil;
while (currRoot != nil) {
newParent = currRoot;
if (newNode.value > currRoot.value) {
// 插入到当前结点的右边
currRoot = currRoot.right;
} else if (newNode.value < currRoot.value) {
// 插入到当前结点的左边
currRoot = currRoot.left;
} else {
// 已经存在该结点,返回
return;
}
}
newNode.parent = newParent;
if (newParent == nil) {
// 新的结点成为根结点
treeRoot = newNode;
} else if (newParent.value > newNode.value) {
// 新结点成为左结点
newParent.left = newNdoe;
} else {
// 新结点成为右结点
newParent.right = newNode;
}
newNode.left = nil;
newNode.right = nil;
newNode.color = red;
fixUp(newNode);
}

private void fixUp(TreeNode newNode) {
TreeNode currNode = newNode;
// 直到结点的父结点颜色为黑色
while (currNode.parent.color == red) {
if (currNode.parent.left == currNode.parent) {
// 当前结点的父结点是左结点
TreeNode uncleNode = currNode.parent.parent.right;
// 如果当前结点的叔结点是红色的,那么可以把红色结点上浮
// 然后新的红色结点成为当前结点继续处理
if (uncleNode.color == red) {
uncleNode.color = black;
currNode.parent.color = black;
currNode = currNode.parent;
} else if(currNode.parent.right == currNode) {
// 如果当前结点是父节点的右子结点,则进行左旋操作
currNode = currNode.parent;
leftRotate(currNode);
// 左旋之后右旋,然后黑结点上浮
currNode.parent.color = black;
currNode.parent.parent.color = red;
rightRotate(currNode.parent.parent);
}
} else {
// 跟上面对称的操作
}
}
treeRoot.color = red;
}

删除

结点的删除比插入要复杂一些,分为两种情况讨论。一种是删除的结点为红色,这种情况下无需对红黑树进行修复操作,因为不影响黑高度。一种是删除的结点为黑色,删除黑结点之后黑高度势必会发生改变,因此要进行调整。

…未完待续

内存分页

内存分页是Linux中的一种内存管理机制,内存被分为固定大小的块,称为页(page)。Linux中的页使用结构struct page来表示。

内存的分配

BUDDY算法

BUDDY算法的作用是减少存储空间中的空洞与碎片,增加利用率。其基本思想是把内存中的所有页面按照2的幂次方进行分块管理,分配的时候如果没有找到相应大小的块,就把大的块一分为二划分为两个小块;释放内存的时候,相邻的块又能够合成新的大块。

SLAB内存管理

BUDDY内存分配的时候,是以页块为基本单位来分配的。但是这种方法的缺点就是粒度比较大,而大多数的内核对象远小于页的大小,因此需要一种更加细粒度的方法来对内存进行管理。由此,Linux引入了SLAB层来进行更加细致的内存管理。

SLAB使用高速缓存来描述不同的内存对象,每种内存对象对应着一个高速缓存。SLAB按照对象的大小进行了分组,在分配的时候不会产生大的内存碎片,并且支持硬件缓存来提高TLB的性能。

为了方便管理,SLAB分为三种状态:full、partial和empty。full表示没有可用的内存对象,partial表示部分使用中,empty表示没有正在使用的内存对象。

SLUB与SLOB

SLUB保留了SLAB的基本思想,但是简化了SLAB中的一些结构,提升内存利用率。而SLOB是一个简化的内存分配器,主要适用于内存非常有限、处理简单的系统。

进程地址空间

内存映射

一个进程运行的时候,其用到文件的代码段、数据段等都是映射到内存地址区域的,这个功能是通过mmap()系统调用来实现的。mmap()将文件从偏移offset的位置开始的长度为length的一个块映射到内存区域中。

缺页错误

虚拟内存进行分配之后,并没有分配相应的物理页面。内核通过缺页错误进行处理,然后进行页表层的创建。

进程

进程是什么?进程是操作系统分配内存、CPU等资源的基本单位。线程这是进程的进一步抽象,一个进程可以分为线程与资源集合。

在Linux中,并没有进程与线程的严格区分,在内核看来线程与进程一样,只不过是与其他一些进程共享内存等某些资源。

进程描述符

在Linux内核中,通过task_struct这个结构体来描述进程,其中包含了许多有关进程的信息,例如进程的状态、线程信息、优先级等。

进程状态

运行TASK_RUNNING

这个状态说明进程可以获取时间片来运行,但是并不意味着该进程的指令正在执行或者获得了CPU资源,只能说是可以被调度执行。

可中断等待TASK_INTERRUPTINLE

进程处于等待状态,不会被调度执行。当等待的资源可用,系统发送一个硬中断,或者受到一个信号都可以唤醒进程进入TASK_RUNNING状态。

不可中断等待状态

与可中断等待的唯一区别就是不能够被信号唤醒。通常用于特殊情况,例如打开设备文件时,相应的设备驱动程序开始探测相应的硬件设备,这个过程中,驱动程序是不能够被中断的。

暂停状态

进程暂停执行

跟踪状态

进程的执行被调试器暂停

僵死状态

表示进程执行被终止,但是其夫进程还未调用wait4或waitpid系统调用来返回有关终止进程的信息。

僵死撤销状态

表示进程的最终状态,父进程已经调用wait4或者waitpid

不可交互等待状态

进程处于等待状态并且无论它时是否可以交互都不提供任何信息。

死亡状态

特殊进程

进程0与进程1是两个特殊进程,进程0是idle进程,当没有其他进程处于TASK_RUNNING状态时,该进程就会被执行。CPU的空闲时间其实就是这个idle进程的运行时间。进程1则是init进程,init进程是用户空间的首个进程,用于孵化其他进程。

进程的内核栈

进程是不断变化的实体,内核为每个进程都分配了一个固定大小的内核栈,用来保存进程在内核态的函数调用信息以及进程描述符。

获取进程描述符

进程描述符可以从内核栈中获取,系统已经定义好了宏用来获取进程描述符。

进程创建

基本步骤:

  1. 查找可执行程序
  2. fork自身
  3. 子进程执行exec()装入该程序

fork()能够让进程复制一个与自身完全一样的进程,exec()则会读取一个外部程序来代替自身。

fork()

fork()会创建一个与自身完全一样的进程,包括各种资源。因此如果不优化fork的话,每次创建进程都会花费很大的开销。

因此,Linux内核对fork进行了优化,使之不完全复制父进程的资源,只复制页表,而且采取了写时复制的策略,让子进程在修改资源的时候复制一份副本,修改之后再回写,这其实跟Java的多线程内存共享机制是一样的。

vfork()

vfork()其实跟优化之后的fork()差不多,只不过vfork()连页表也不会复制。

clone()

clone()fork()最大的不同是,clone()可以选择性地复制父进程的资源,甚至可以让旧进程与新进程不再是父子关系,而是兄弟关系。

内核线程

内核线程就是运行在内核态的线程,普通的线程进入到内核态需要经过系统调用,但是内核线程不需要。

中断

内核需要管理系统中存在的各种设备的状态,比如鼠标是否按下了,键盘是否按下了,按下了什么健等。通常来说,这种查询状态的可以用轮询来实现,但是缺点太多了,可能设备状态没有得到读取,也可能设备状态一直没有变化但还是维持轮询状态,浪费资源。

因此引入了中断机制。中断机制是设备在需要的时候通知内核,让内核能够得到相应的信息,然后再作出处理的一种机制。

同步中断

同步中断是由CPU自身产生的中断信号,只有在指令执行完成之后才会发出中断信号。
通常来说,同步中断成为异常,异常又产生于CPU执行过程中的指令,比如除0。异常又分为故障、陷阱与终止。

故障

在引起异常的指令之前,将异常报告给系统,处理之后再返回到之前执行的位置。

陷入

陷入是在引起异常的指令之后,将异常情况通知给系统处理。

终止

终止是系统出现严重情况的时候,通知系统的异常。产生终止时,被执行的指令是不能恢复正常执行的,比如说硬件故障引起的异常。

异步中断

异步中断是由其他设备产生的中断,可以在指令执行的任何时候产生。

可屏蔽中断

可屏蔽中断是CPU可以选择屏蔽掉的中断信号,比如打印机中断之类的信号。这类中断需要通过中断控制器来发送中断信号给CPU

不可屏蔽中断

这类中断是不可以屏蔽的,一旦产生,CPU必须进行处理。

中断的处理

中断在产生之后,根据中断源说提供的中断向量,从中断描述符表中获取相应的处理程序地址,然后执行。

中断向量表

实模式下的中断向量表由中断向量构成,每个中断向量对应着一个处理程序的入口。

进入保护模式之后,中断向量表改名为中断描述符表,每个表项成为门描述符意味着中断发生的时候,必须先经过的检查,然后才能进入中断处理程序。

中断服务程序

中断服务程序必须在设备驱动程序中定义,并且在使用request_irq申请IRQ线的时候,关联到申请的IRQ线上。

中断服务程序的返回值是一个特殊值-irqreturn_t,当为IRQ_NONE(0)时,表示不处理所接收到的中断请求;当为IRQ_HANDLED(1)时,表示接收到了正确的中断请求,并做出了正确的处理。

中断服务程序是不可重入的,也就是说当前有中断服务程序正在执行时,相应中断线上的所有处理器都会被屏蔽,保证同一个中断程序不会被同时调用。

重要数据结构

中断描述符irq_desc

数据结构irq_desc用于描述IRQ线的属性与状态,成为中断描述符。每一个IRQ都有自己的irq_desc对象,所有的对象组织在一起成为一个数组。在2.6.0版本中,irq_desc结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct irq_desc {
/* IRQ的状态 */
unsigned int status; /* IRQ status */
hw_irq_controller *handler;
/* IRQ产生中断时需要调用的程序服务 */
struct irqaction *action; /* IRQ action list */
unsigned int depth; /* nested irq disables */
/* 中断发生的次数 */
unsigned int irq_count; /* For detecting broken interrupts */
/* 未处理的中断数 */
unsigned int irqs_unhandled;
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;

中断控制器描述符irq_chip

用于描述不同类型的中断控制器。

中断服务程序描述符irqaction

多个设备可以共享一个IRQ线,所以使用irqaction来区分不同的设备。在上述的irq_desc结构体中,有这么一段:

1
struct irqaction *action

这就是使用该IRQ线的设备队列,该队列中的所有中断服务程序将被依次执行。

中断子系统初始化

Linux的中断处理机制主要包括三个方面的内容:

  • 中断子系统初始化,初始化中断描述符表等。
  • 中断或异常处理,实际的中断处理过程。
  • 中断API,提供一组API给驱动程序调用。

中断描述符的初始化

知道是在内核引导阶段以及初始化阶段执行的就可以了。

中断请求队列的初始化

也是在内核初始化阶段处理的。

中断或者异常的处理

  • 中断处理流程:设备产生中断,通过中断控制器将中断信号发送给CPU处理,然后获取中断向量号找到相应的门描述符,从而获取中断服务程序的地址并执行。
  • 异常处理流程:异常不需要通过中断控制器发送电信号,只需要CPU找到相应的门描述符,获取相应的异常服务处理程序。

中断API

内核提供了一组接口用于控制系统上的中断状态,开发驱动需要了解这些接口的使用。

request_irq函数

request_irq的主要任务是为IRQ线的中断请求队列创建上文提到的irqaction结点。此函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
int request_irq(
/* 要申请的IRQ线 */
unsigned int irq,
/* 中断处理方法 */
irqreturn_t (*handler)(int, void *, struct pt_regs *,),
/* irq类型 */
unsigned long irq_flags,
/* 设备名 */
const char * devname,
/* 设备ID */
void *dev_id
)

free_irq函数

free_irq函数的作用与request_irq函数的作用刚好相反,是从设备描述符中移除相应设备的结点。原型如下:

1
void free_irq(unsigned int irq, void *dev_id)

激活与禁止

通常来说,中断处理程序处理中断的时候,如果想要避免并发带来的问题,则需要禁止中断线确保不会抢占代码。

函数 描述
local_irq_disable() 禁止当前CPU的中断
local_irq_enable() 激活当前CPU的中断
local_irq_save() 禁止当前CPU的中断并保存标志寄存器的内容
local_irq_restore() 恢复当前CPU的中断状态,必须与local_irq_save()函数处于同一函数当中
disable() 禁止指定的中断线,返回前会确保当前中断线上的所有中断处理程序已经退出
enable_irq() 激活指定的中断线
disable_irq_nosync() 禁止指定的中断线,但不会确保当前中断线的中断处理程序已经退出
in_irq() 判断内核是否在执行中断函数
in_softrq() 判断是否正在处理软中断
in_interrupt() 判断是否处于中断上下文,比如正在执行中断程序或者下半部处理程序

多处理器系统中的中断处理

处理器间中断

现代操作系统中,通常需要多个处理器之间进行协调工作,这是通过处理器间中断IPI来实现的。

中断亲和力

指将一个或者多个中断服务程序绑定到特定的CPU上去运行。

中断负载均衡

将重负载的CPU上的中断转移到比较空闲的CPU上进行处理。

中断的下半部

为了解决一次响应需要处理的大量数据的中断,Linux将中断的工作划分为两个部分,即中断的上半部和下半部。上半部是实际响应中断的程序,下半部是是其他一些可以延缓处理的部分,在下半部分处理期间,中断还是打开的。例如网卡的中断接收过程就是这样。

系统调用

什么是系统调用

系统调用是内核提供给上层应用使用内核功能的接口,能够操纵硬件、访问各种系统资源等。

为什么需要系统调用

应用程序运行在用户态,而内核运行在内核态,如果用户态的软件直接操作硬件的话,就会存在安全问题。因此,必须通过内核提供的接口来间接操作硬件、访问各种资源等

POSIX

POSIX其实是一个标准,定义了各种所需要提供的服务。

系统调用表

系统调用表提供了所有系统服务所对应的服务的函数地址。例如,对于x86-64结构的CPU来说,系统调用表在arch/x86_64/kernel/syscall.c里。

当然了,光有系统调用表还是不够的,实际上系统调用是根据系统调用号来执行的。x86-64架构的调用号在include/asm-x86_64/unistd.h下。2.6.0内核中的系统调用仅有236个。

系统调用流程

用户态的应用并不是直接调用系统调用函数的,而是通过函数封装来调用。例如在Linux中,就提供了C语言的系统调用接口。所以大致的流程就是:

1
应用程序 -> C库函数 -> 系统调用

用户态到内核态

系统调用的执行需要从用户态到内核态的转换(因为是从应用代码到内核代码)。通过系统陷入指令,可以完成这种转换。在Linux中,系统陷入是通过软中断来实现的(int $0x80)。

通过软中断,系统会跳转到系统调用的处理程序,然后就可以访问系统调用了。

Shell

变量

声明变量注意,变量名不加$符号,而且变量名和等号之间一定不能有空格!

具体规则如下:

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字(可用help命令查看保留关键字)。

除了显式地直接赋值,还可以用语句给变量赋值,如:

1
2
3
for file in `ls /etc`

for file in $(ls /etc)

使用变量

使用一个定义过的变量,在变量名前加$即可引用

1
2
3
name="huang"
echo $name
echo ${name}

给引用的变量加上花括号,是一个好习惯。

只读变量

使用readonly可以使变量成为只读变量:

1
2
3
#!/bin/bash
myUrl="https://www.google.com"
readonly myUrl

删除变量

使用 unset 命令可以删除变量。语法:

1
unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

拼接字符串

1
2
3
4
5
6
7
8
9
your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
echo $greeting_2 $greeting_3

获取字符串长度

1
2
string="abcd"
echo ${#string} #输出 4s

提取字符串

以下实例从字符串第 2 个字符开始截取 4 个字符:

1
2
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

注意:第一个字符的索引值为 0

查找字符串

查找字符 io 的位置(哪个字母先出现就计算哪个):

1
2
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4

Shell数组

bash支持一维数组,不支持多维数组,没有限定数组大小,而且使用空格分开元素

定义数组

1
arr=("val1" "val2" "val3")

读取数组

和读取变量类似,要加$引用,使用[]引用元素,使用@符号获取所有元素,获取长度和字符串类似

1
2
3
4
5
6
7
8
arr=("val1" "val2" "val3")
echo ${arr[0]}
# get length of arr
echo ${#arr[@]}
# or
echo ${#arr[*]}
# or get single length of element in arr
echo ${#arr[n]}

多行注释

多行注释还可以使用以下格式:

1
2
3
4
5
:<<EOF
注释内容...
注释内容...
注释内容...
EOF

Shell传递参数

在向脚本传递参数的时候,使用$n获取参数,其中n为数字,代表第n个参数。

注意:与数组不同,参数从1开始

其他特殊字符处理

参数处理 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。 如”$*”用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。 如”$@”用「”」括起来的情况、以”$1” “$2” … “$n” 的形式输出所有参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

Shell基本运算

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awkexprexpr 最常用。

expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

算数运算符

下表列出了常用的算术运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
+ 加法 expr $a + $b 结果为 30。
- 减法 expr $a - $b 结果为 -10。
* 乘法 expr $a \* $b 结果为 200。
/ 除法 expr $b / $a 结果为 2。
% 取余 expr $b % $a 结果为 0。
= 赋值 a=$b 将把变量 b 的值赋给 a。
== 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
!= 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

注意: 条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

布尔运算符

下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

逻辑运算符

以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true

字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”:

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n “$a” ] 返回 true。
$ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

文件测试运算符

文件测试运算符用于检测 Unix 文件的各种属性。

属性检测描述如下:

操作符 说明 举例
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。

其他检查符:

  • -S: 判断某文件是否 socket。
  • -L: 检测文件是否存在并且是一个符号链接。

Echo

开启转义

1
echo -e "hello \n world"

原样输出字符串,不进行转义或取变量(用单引号)

1
echo '$name\"'

Shell流程控制

if语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if condition
then
...
fi
#or
if condition
then
...
else
...
if
#or
if condition
then
...
elif condition1
then
...
else
...
fi

for

1
2
3
4
for var in item1 item2 ... itemN
do
...
done

例如,想要显示/etc目录下的所有文件

1
2
3
4
5
6
f="/etc"
fs=`ls $f`
for file in $fs
do
echo $file
done

while

1
2
3
4
while condition
do
...
done

无限循环

1
2
3
4
5
6
7
8
9
10
11
while :
do
...
done
#or
while true
do
...
done
#or
for (( ; ; ))

until

1
2
3
4
until condition
do
...
done

case

1
2
3
4
5
6
7
8
case val in
mode1)
...
;;
mode2)
...
;;
esac

Shell函数

1
2
3
4
5
6
#definition
[ function ] funname [()]
{
...
[return int;]
}

参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数…

函数参数用法跟shell传递参数一样

编译器做了什么

通过查看编译后产生的Java文件,观察其结构

构造后的AIDL类产生了一个同名接口,这个接口包含了AIDL内声明的所有方法,并且继承了android.os.IInterface。接口包含了两个静态类:

  • class Default
  • class Stub

Default类默认实现了AIDL接口以及IInterface的asBinder()方法,但都为空实现。

Stub类为具体的AIDL实现类,继承了android.os.Binder类,并实现AIDL接口,接下来看下这个类的结构以及这个类都做了些什么工作。

  • 构造函数。构造函数将AIDL接口与Binder通过一个描述符关联起来,在Binder中,这个attachInterface()是这么描述自身功能的:

    将特定的接口与Binder绑定(其实就是内部变量持有了这个AIDL接口),当调用这个函数之后,使用queryLocalInterface()函数将会返回与Binder绑定的IInterface,即为编译器实现好的AIDL接口的实现类。

  • 一个静态方法,将Binder转换为IInterface,即我们的AIDL接口,其实就是从Binder中查找Binder是否关联了与描述符相等的IInterface接口:

    1
    2
    3
    4
    5
    6
    public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
    if (mDescriptor != null && mDescriptor.equals(descriptor)) {
    return mOwner;
    }
    return null;
    }

如果本地的Binder已经与我们的AIDL接口实现绑定了之后,就直接返回AIDL的实现类,否则,就需要通过代理(这个代理本身也实现了AIDL接口)来连接到Binder。

  • asBinder()方法。返回Binder本身。
  • onTransact()方法。

onTransact()方法就是实现进程间通信的关键方法。这个方法需要四个参数。一个是整型的code,表示需要调用的方法。第二个参数为data,表示远程调用中传入的参数。第三个参数为reply,表示执行后返回的数据。第四个参数为flags,这个参数传入其父类Binder的onTransact方法中,表示RPC的类型。

在onTransact()方法中,如果调用的是AIDL中的方法的时候,就会将返回数据写入reply中。

  • 接下来讲讲代理类Stub.Proxy。

Proxy实现了AIDL接口,其中的AIDL方法是在将参数数据写入data之后,就调用Binder的transact方法,transact方法其实就是调用了onTransact()方法。

Android M 6.0

增加了运行时的权限申请

Android N 7.0

强制执行StrictMode API,Intent的Uri中scheme不能为file类型。如果要共享文件,需要使用content://类型的data。如果要共享文件,则使用FileProvider。

官方FileProvider说明

官方是这么说明FileProvider的:

FileProvider是ContentProvider的一个特殊子类,方便了应用间的安全的应用内文件共享,通过创建一个content://的Uri而不是一个file://类型的Uri。

使用FileProvider需要经过以下的几个步骤:

  • 声明一个FileProvider
  • 确定可用的文件
  • 获取文件的Uri
  • 为Uri授予临时权限
  • 把Uri提供给其他应用

官方说明:在授权的时候,需要注意的是如果设备的运行API级别位于Android 4.1 (API Level 16)与Android 5.1(API Level 22)之间,需要创建一个ClipData对象,并为这个ClipData对象授予权限

1
2
3
shareContentIntent.setClipData(ClipData.newRawUri("", contentUri));
shareContentIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

Android O 8.0

Android 8.0引入了通知Channel,允许为要显示的每种通知类型创建用户可自定义的渠道。

后台限制执行

现在,在Manifest里面注册的广播接收器无法在后台使用。

未知软件源限制

Android 8.0移除了“允许未知来源”的开关。如果想安装应用,则需要申请权限

1
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

注意:这里申请权限需要跳转到“允许安装未知应用”界面进行授权

1
2
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);

Android P 9.0

Http请求失败

9.0中的默认禁止了Http请求,需要改用Https。可以添加网络配置:

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

然后在Manifest中声明:

1
android:networkSecurityConfig="network_security_config"

可以指定特定域名:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Android 9.0 上部分域名时使用 http -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">secure.example.com</domain>
<domain includeSubdomains="true">cdn.example1.com</domain>
</domain-config>
</network-security-config>

前台服务

想创建前台服务,需要申请FOREGROUND_SERVICE权限

设备序列号

在 Android 9 中,调用Build.SERIAL 会始终返回 UNKNOWN 以保护用户的隐私。如果你的应用需要访问设备的硬件序列号,那么需要先请求 READ_PHONE_STATE 权限,然后调用 Build.getSerial

Android Q 10

文件的存储

Android 10的文件存储机制改为了沙盒模式,只能访问自己目录下的文件和公共媒体文件,但是10以下的机型还是使用的老式的文件存储方式。

应用私有目录

应用的外部私有目录对应/Android/data/package_name/,内部对应了/data/data/package_name。应用的私有目录文件访问方式与之前的版本保持一致,不需要改动。

共享目录

包含媒体文件、文档以及其他文件。

  • 共享目录需要通过MediaStore API或者Storage Access Framework的方式访问。
  • MediaStore API在共享目录下创建文件或者访问应用自己创建的文件,是不需要申请存储权限的。
  • MediaStore API访问其他应用在共享目录下创建的文件,需要申请存储权限,否则通过ContentResolver查询不到文件的Uri。
  • MediaStore API不能访问其他应用创建的非媒体文件,如pdf、office、doc、txt等,只能通过Storage Access Framework方式访问。

如果应用还未完成适配工作,可以暂时让应用以兼容模式运行,需要在Manifest中声明:

1
android:requestLegacyExternalStorage="true"

然后在代码中判断是否是兼容模式:

1
Environment.isExternalStorageLegacy()

存储访问框架

  • 调用存储相关操作的Intent;
  • 用户看到系统选择器,供其浏览文档提供其并选择将执行存储文件相关的位置或文档;
  • 应用获得对代表用户所选位置或文档的URI的读写访问权限。

但是发现一个不方便的地方:如果采用Storage Access Framework的话,保存文件的时候只能通过打开窗口来选择保存的地方,不能直接在后台完成所有操作。

如果是在10.0之前的话,应该可以直接通过访问文件来读写文件,只要获取了写权限。但是现在10之后,不需要写权限了,各个应用是拥有沙箱机制的。

定位权限

如果需要使用后台位置权限,则需要声明新的权限ACCESS_BACKGROUND_LOCATION

媒体文件位置权限

图片之类的文件会带有位置信息,而这个信息是敏感的。在Android 10之后如果要访问媒体位置,则需要声明ACCESS_MEDIA_LOCATION 权限。

Android R 11

分区存储

从Android 11开始执行强制分区存储机制,之前Android 10中声明的android:requestLegacyExternalStorage="true"已经不起作用了。

另外,Android 11允许使用除了MediaStore API之外的API通过文件路径直接访问存储空间中的媒体文件,包括:

  • File API
  • 原生库,如fopen()

单次权限授予

就是短时间内授予用户权限。

请求位置权限

Android 11中取消了“始终允许”这一选项,也就是不会授予后台访问位置权限。建议递增位置权限请求,比如先请求前台的权限,再在某一需要的时候请求后台权限。

软件包可见性

新特性,用于限制其他软件获取用户当前安装的软件信息。

前台服务类型

如果需要在前台访问位置信息,则需要在Manifest中声明相应的前台服务类型。

权限自动重置

如果用户的应用经过数个月的时间没有使用的话,那么系统就会自动清除该应用的运行时数据来保护用户的隐私。

读取手机号

如果要读取手机号的话,需要在Manifest中声明READ_PHONE_STATE权限。

关于Android11下单编Framework的问题

在Android 11下,不知道为什么原本我在Android 6.0下使用的mm出现了错误(会把test也一并编译,导致问题),因此更换为下面的命令:

make -j16 SystemUI

make -j16 framework or make -j16framework-minus-apex

make -j16 services