Framework篇 第一章 Linux

  本章来介绍一下Linux系统基础知识,主旨是为大家日后深入学习Linux做铺垫。

第一节 基础入门

为什么要学习Linux

  Linux应用广泛,从嵌入式设备、服务器领域到超级电脑,它都发挥着相当重要的作用。

-  在嵌入式领域,流行的TiVo数字视频录像机还采用了定制的Linux,思科在网络防火墙和路由器也使用了定制的Linux。Linux也用于舞台灯光控制系统,如WholeHogIII控制台。在智能手机、平板电脑等移动设备方面,基于Linux内核的Android操作系统已经成为当今全球最流行的智能手机操作系统。
-  在服务器领域,根据2006年9月Netcraft的报告显示,十个最大型的网络托管公司有八个公司在其Web服务器运行Linux发行版。Linux发行版是构成LAMP(Linux操作系统,Apache,MySQL,Perl / PHP / Python)的重要部分,LAMP是一个常见的网站托管平台,在开发者中已经得到普及。

  因此笔者可以毫不负责任的说,一个程序员如果对Linux一窍不通的话,是说不过去的,更何况我们还是Android程序员。

Linux常识

  如果你想了解Linux历史的话,可以去看看《Linux入门很简单》一书,同时给大家推荐一个Linux学习网站: 实验楼

发行版与内核
  刚开始接触Linux时你一定会看到Linux发型版Linux内核这两个概念:

1、操作系统内核,通俗地说就是操作系统最核心最关键的部件,它负责一些最基本的工作,比如:管理硬件驱动、管理内存、管理文件系统、管理进程等等;这些工作只要少了任何一样,整个操作系统都没法运转。
2、操作系统都有"内核"的概念,即Linux 和 Windows 都有其内核。
3、但如果你使用 Windows,通常感觉不到"内核"的存在。为啥捏?原因在于:微软是把 Windows 当作一个整体来发布的。对于普通用户而言,你拿到的是一个完整的操作系统,所以你感觉不到"内核"的存在。而 Linux 不同于其它操作系统的地方在于:Linus 领导的开源社区只负责开发内核,不开发其它的东西(比如:运行库、图形界面、应用软件等)。
4、这就引出一个问题——光有一个赤裸裸的内核,用户是没法用的(就好比你光拿到一个汽车引擎,你是没法开车的)。为此,就有一大帮开源社区或商业公司,在这个裸露的内核外面,再包上一些东西(比如:运行库、应用软件)。经过这样包装之后,就成为"发行版"。为啥 Linux 的发行版如此之多捏?前面俺说了,Linux 内核是彻底开放的,随便什么阿猫阿狗都可以去 Linux 的官网下载到内核。于是,发行版自然就很多啦。


通用发行版 VS 专用发行版

1、所谓的"通用发行版",顾名思义就是:这个发行版可以派上各种用场;反之,"专用发行版"是为特定用途设计,只能用于某些特定场合。
2、通用发行版名气比较大的有:Debian(非常强调"自由"的开源理念,它有很多衍生的发行版,形成一个大家族。)、Fedora、Slackware等。
3、面向客户端(桌面)的专用发行版名气比较大的有:Ubuntu Desktop(衍生自Debian)、Mageia等。Ubuntu Desktop以发布时间做版本号(比如13.10 就是2013年10月发布)。每半年发布一个版本。它的版本分两种:普通版本和长期支持版本(LTS)。LTS 会持续提供支持(补丁更新)长达几年(桌面版 3年,服务器版 5年),普通版本只支持9个月。
4、面向服务端的专用发行版名气比较大的有:Red Hat Enterprise Linux(简称 RHEL)、CentOS(从 RHEL 衍生)、Ubuntu Server(从 Debian 衍生)等等。


Shell

-  Shell是一个用C语言编写的应用程序,它提供了一个用户界面,用户通过在这个界面输入命令来访问操作系统内核的服务。
   -  Linux下,很多工作都是通过命令完成的,学好Linux,首先要掌握常用Shell命令。
-  为了防止重复编写代码,我们将一组Shell命令写在文件中,每次需要的时候就执行一下文件即可,这个文件就被称为Shell脚本。
-  在UNIX/Linux中比较流行的Shell工具有bash,zsh,ksh,csh等等,Ubuntu终端默认使用的是bash。


内核版本

-  内核版本指的是在Linus领导下的开发小组开发出的系统内核的版本号。Linux的每个内核版本使用形式为x.y.zz-www的一组数字来表示。其中:
   -  x.y:为linux的主版本号。通常y若为奇数,表示此版本为测试版,系统会有较多bug,主要用途是提供给用户测试。
   -  zz:为次版本号。
   -  www:代表发行号(注意,它与发行版本号无关)。
-  当内核功能有一个飞跃时,主版本号升级,如 Kernel2.2、2.6等。而内核增加了少量补丁时,常常会升级次版本号,如Kernel2.6.15等。


体系结构

-  Linux从内到外依次分为四个层次:
   -  Hardware层:各类硬件,如硬盘、CPU等。
   -  Kernel层:内核直接与硬件交互,并处理大部分较低层的任务,如内存管理、进程调度、文件管理等。
   -  Shell层:Shell是一个处理用户请求的工具,它负责解释用户输入的命令,调用Kernel层提供的功能。如cp、mv、cat和grep等。
   -  Application层:各类应用程序,如:DBMS、Mail、FTP。



本节参考阅读:

文件系统

分区与挂载

  与Windows一样,Linux中同样存在分区的概念,那么硬盘为什么要有分区呢?

-  有利于管理,系统一般单独放一个区,这样由于系统区只放系统,其他区不会受到系统盘出现磁盘碎片的性能影响。
-  如果一个分区出现逻辑损坏,仅损坏的分区而不是整个硬盘受影响。
-  避免过大的日志或者其他文件占满导致整个计算机故障,将它们放在独立的分区,这样可能只有那一个分区出现空间耗尽。
-  大硬盘搜索范围大,效率低。
-  在运行Unix的多用户系统上,有可能需要防止用户的硬连结攻击。为了达到这个目的,/home和/tmp路径必须与如/var和/etc下的系统文件分开。

  与Windows中每个分区对应一个盘(“C盘”、“D盘”)的情况不同,在Linux系统中普通用户是感觉不到分区的,Linux将整个文件系统看做一棵树,这棵树的树根叫做根文件系统,用“/”表示。
  各个分区通过“挂载”(Mount)以文件夹的形式被放入到“/”下面。

-  挂载是指将一个硬件设备(例如硬盘、U盘、光盘等)对应到一个已存在的目录上。 若要访问设备中的文件,必须将设备挂载到一个已存在的目录上, 然后通过访问这个目录来访问存储设备。

  也就是说整个系统的所有文件,对于普通用户来说,都是放在“/”下的,“/”主要的目录有如下几个:

/bin:存放操作系统运行所需要的可执行文件,所有用户都有权访问,例如:cat、ls、cp等命令。
/boot:存放启动Linux时使用的一些核心文件,例如:kernal(系统内核)、initrd等。
/dev:存放系统中的设备,从此目录可以访问各种系统设备,如磁盘驱动器,调制解调器,CPU,USB等。
/etc:存放系统和应用软件的配置文件。
/home:存放普通用户的个人文件。每个用户的主目录均在/home下以自己的用户名命名。
/lib:存放/bin和/sbin中二进制文件所需要的库文件。
/media:可移动设备的挂载点(如CD-ROM)。
/mnt:临时挂载的文件系统。
/opt:多数第三方软件默认安装到此位置,但并不是每个系统都会创建这个目录。
/proc:虚拟文件系统,里面保存了内核和进程的状态信息,多为文本文件,可以直接查看。如/proc/cpuinfo保存了有关CPU的信息。
/root:这是根用户的主目录。与保留给个人用户的/home下的目录很相似,该目录中还包含仅与根用户有关的条目。
/sbin:root用户才能使用的系统二进制文件,例如: init、 ip、 mount等。
/tmp:该目录用来保存临时文件,在系统重启时目录中文件不会被保留。
/usr:用于存储只读用户数据的第二层次; 包含绝大多数的(多)用户工具和应用程序。
/var:变量文件——在正常运行的系统中其内容不断变化的文件,如日志,脱机文件和临时电子邮件文件。有时是一个单独的分区。


  范例1:查看当前系统中的分区情况。

1
2
3
4
5
6
7
8
9
10
11
12
# 使用df命令查看分区情况,-h参数表示以更容易阅读的方式显示结果,你可以去掉-h然后对比一下。
df -h

文件系统 容量 已用 可用 已用% 挂载点
udev 7.8G 4.0K 7.8G 1% /dev
tmpfs 1.6G 1.4M 1.6G 1% /run
/dev/sda3 883G 220G 619G 27% /
none 4.0K 0 4.0K 0% /sys/fs/cgroup
none 5.0M 0 5.0M 0% /run/lock
none 7.8G 108M 7.7G 2% /run/shm
none 100M 44K 100M 1% /run/user
/dev/sda1 496M 26M 471M 6% /boot/efi

语句解释:
-  分区(如U盘等)必须挂载到一个目录下才能使用。
-  上面的第一列是分区的名称,对于不同的硬盘,分区的名称不一样:
   -  对于IDE硬盘,名称以hd为前缀,后面跟着盘号(a、b、c、d),还有分区号(1、2、3、4)。
   -  对于SCSI硬盘,名称以sd为前缀,其它与IDE相同。



本节参考阅读:

文件与目录

  Linux中的所有数据都被保存在文件中,而且所有的文件被分配到不同的目录。
  Linux有三种基本的文件类型:

-  普通文件。普通文件是以字节为单位的数据流,包括文本文件、源码文件、可执行文件等。
-  目录。相当于Windows和Mac OS中的文件夹。
-  设备文件。Linux 与外部设备(例如光驱,打印机,终端,modern等)是通过一种被称为设备文件的文件来进行通信。Linux 和一个外部设备通讯之前,这个设备必须首先要有一个设备文件存在。
   -  设备文件和普通文件不一样,设备文件中并不包含任何数据,即0字节。
   -  设备文件有两种类型:字符设备文件和块设备文件。
   -  字符设备文件的类型是"c"(具体后述),字符设备文件向设备传送数据时,一次传送一个字符。典型的通过字符传送数据的设备有终端、打印机等。
   -  块设备文件的类型是"b",块设备文件向设备传送数据时,先从内存中的buffer中读或写数据,而不是直接传送数据到物理磁盘。
   -  磁盘和CD-ROMS既可以使用字符设备文件也可以使用块设备文件。


  接下来介绍几个文件相关的常用Shell命令。

  范例1:文件操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用mkdir命令,在当前目录下创建一个名为myDir的文件夹。
# 在mkdir命令后面跟随“-p”可以连父目录一起创建(如果不存在的话)。
mkdir myDir

# 使用cd命令,进入到myDir文件夹中。
cd myDir

# 使用touch命令创建文件,如果想同时创建多个,那多个文件名之间使用空格间隔。
touch b.txt c.txt

# 使用ls命令,列出当前文件夹下的所有文件。在ls命令后面跟随“-l”参数可以同时把文件的详细信息列出来。
ls

# 使用rm命令,删除文件。其中*是通配符,表示删除所有文件,但不能删文件夹。
rm *

# 使用pwd命令查看当前所处的目录。
pwd

语句解释:
-  上面只是简单的介绍了各个命令,一般情况下每个命令都可以接受若干个参数。
-  比如若你想使用rm命令删除一个文件夹,可以使用“rm -r myDir”,其中-r会删除myDir以及其内的所有文件。
-  各命令的语法就不细说了,网上很容易搜索到。


  范例2:查找命令。

1
2
3
4
5
6
7
8
# 使用find命令查找文件,下面命令的含义:从当前目录下的test目录中查找后缀名为txt的文件。
# 其中-name参数用来告诉find命令按照文件名去查找,言外之意就是find命令还可以按照其它方式查找,比如文件权限等。
# 如果不指定查找的目录,则默认使用当前目录。
find test -name *.txt

# 使用grep命令查找文件里的内容,下面命令的含义:
# 从test目录下的所有txt文件中搜索Hello关键字。
grep Hello test/*.txt

语句解释:
-  find和grep命令有很多附加参数,这里没法一一介绍,各位请自行搜索。
-  在grep命令后面加上-n参数可以把行号给列出来。


  范例3:复制、移动、重命名。

1
2
3
4
5
6
# 使用cp命令复制文件,下面命令的含义:将当前目录下的b.txt复制到上一级目录中,并将复制过去的文件改名为bb.txt。
# 如果你想复制文件夹,可以在后面加一个-r参数。
cp b.txt ../bb.txt

# 使用mv命令移动文件。
mv b.txt ../

语句解释:
-  也可以用mv命令给文件和文件夹重命名,比如mv b.txt newb.txt。
-  批量重命名可以使用rename命令,具体请自行搜索。


  范例4:查看与编辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用cat命令,可以把文件的全部内容给拿出来,使用-n参数可以加上行号。
cat -n result.txt

# 使用nl命令,也可以把文件全部内容给拿出来,但它在打印行号的功能上,比cat更专业。
# 这个命令有-b(设置是否给空行编号)和-n(设置行号的显示位置以及是否补0)两个参数,具体请自行搜索。
nl -n rz result.txt

# 使用more和less命令分页查看文件。
# 其中more命令打开文件后默认只显示一屏内容,可以使用Enter键向下滚动一行,使用Space键向下滚动一屏,按下h显示帮助,q退出。
more result.txt

# 使用head和tail命令,它们一个是只查看的头几行(默认为10行,不足10行则显示全部),另一个是只查看尾几行。
# 加上-n参数可以设置查看多少行。
# 关于tail命令,它有一个很牛的参数-f,这个参数可以实现不停地读取某个文件的内容并显示,这可让我们动态查看日志起到实时监视的作用。
head result.txt

# 使用file命令查看文件的类型。
file result.txt

语句解释:
-  cat不适合打开大文件(比如有成百上千行的文件),大文件推荐使用vim。

用户及文件权限管理

  Linux 是一个可以实现多用户登陆的操作系统,比如“李雷”和“韩梅梅”都可以同时登陆同一台主机,他们共享一些主机的资源,但是由于 Linux 的 用户管理 和 权限机制 ,不同用户不可以轻易地查看、修改彼此的文件。
  下面我们就来学习一下 Linux 下的账户管理的基础知识。

  首先,来看看“用户”和“用户组”的概念:

-  在Linux中,系统中默认就存在了很多用户(主要是系统用户),而且每个用户都有一个归属(用户组)。
-  用户组简单地理解就是一组用户的集合,它们共享一些资源和权限,同时拥有私有资源。
-  用户组就跟家的形式差不多,你的兄弟姐妹(不同的用户)属于同一个家(用户组),你们可以共同拥有这个家(共享资源),爸妈对待你们都一样(共享权限)。
-  你偶尔写写日记,其他人未经允许不能查看(私有资源和权限)。
-  当然一个用户是可以属于多个用户组的,正如你既属于家庭,又属于学校或公司。


  范例1:查看所有用户以及所有用户组。

1
2
3
4
5
6
# cut是一个选取命令,主要用来截取字符串。下面代码的含义为:
# 依次检查/etc/passwd文件中的每一行,将行内的数据按照“:”字符拆分成若干组,然后把第1组给显示出来。
cut -d : -f 1 /etc/passwd

# 相应的如果想知道系统中当前有多少个用户组,则可以执行:
cut -d : -f 1 /etc/group


  范例2:查看当前用户。

1
2
3
4
who am i

输出:
cutler pts/0 2016-07-19 15:24 (:0)

语句解释:
-  输出的第一列表示打开当前伪终端的用户的用户名(要查看当前登录用户的用户名,去掉空格直接使用 whoami 即可)。
-  第二列的 pts/0 中 pts 表示伪终端,你每打开一个终端就会产生一个伪终端, pts/0后面那个数字就表示打开的伪终端序号,你可以尝试再打开一个终端,然后在里面输入 who am i ,看第二列是不是就变成 pts/1 了。
-  第三列则表示当前伪终端的启动时间。


  范例3:查看指定用户所在的用户组。

1
2
3
4
groups cutler

输出:
cutler : cutler adm cdrom sudo dip plugdev lpadmin sambashare

语句解释:
-  其中冒号之前表示用户,后面表示该用户所属的用户组,可以看到cutler拥有8个用户组。
-  每次新建用户如果不指定用户组的话,默认会自动创建一个与用户名相同的用户组。


  然后,来看看“root”用户的概念:

-  在Linux系统里, root用户拥有整个系统至高无上的权利,比如添加/删除用户。所有对象它都可以操作,所以很多黑客在入侵系统的时候,都要把权限提升到root权限。
-  另外在Android中获得root权限之后就意味着已经获得了手机的最高权限,这时候你可以对手机中的任何文件(包括系统文件)执行任意操作。
-  我们一般登录系统时都是以普通账户的身份登录的,当需要执行root用户才能执行的操作时(比如创建用户),就要用到sudo这个命令了。
-  不过使用这个命令有两个大前提,一是你要知道当前登录用户的密码,二是当前用户必须在sudo用户组。


  范例4:创建新用户。

1
sudo adduser huye

语句解释:
-  使用adduser命令可以创建一个新用户。笔者当前登陆的用户是cutler,它并不是root用户,为了让cutler可以创建用户,就在adduser命令之前加上了sudo命令。
-  接着按照提示给新用户设置密码,后面的选项的一些内容你可以选择直接回车使用默认值。
-  adduser命令不但可以添加用户到系统,同时也会默认为新用户创建home目录。
   -  执行“ls /home”命令就可以看到。
-  使用“su 用户名”可以切换用户,切换完之后就可以使用“whoami”命令来验证,还可以去测试新用户是否可以执行sudo命令。
-  退出当前用户跟退出终端一样可以使用exit命令。


  范例5:给新用户添加sudo用户组。

1
2
3
4
sudo usermod -G sudo huye

# 使用下面的命令可以删除用户。
# sudo deluser huye --remove-home

语句解释:
-  使用usermod命令可以为用户添加用户组,同样使用该命令你必需有root权限。
-  你可以直接使用root用户为其它用户添加用户组,或者用其它已经在sudo用户组的用户使用sudo命令获取权限来执行该命令。
-  然后再登陆到huye上后,就可以使用sudo命令了,而且通过“groups huye”命令也可以看到它已经被添加到sudo组里了。


  最后,我们来看看Linux文件权限相关的知识。


  范例6:查看文件的权限。

1
2
3
4
5
6
7
8
9
10
# -l表示查看详细信息
ls -l

总用量 192
drwxrwxr-x 12 cutler cutler 4096 7月 14 11:35 a
-rw-rw-r-- 1 cutler cutler 1562 6月 20 09:00 a.py
-rw-rw-r-- 1 cutler cutler 33850 6月 15 19:19 cuihu_1.xlsx
-rw-rw-r-- 1 cutler cutler 141824 6月 17 11:21 cuihu_2.xls
drwxrwxr-x 4 cutler cutler 4096 7月 14 11:39 jira384229
drwxrwxr-x 5 cutler cutler 4096 7月 14 14:15 monkeytest

语句解释:
-  命令输出了7部分内容,从左到右依次为:文件的类型和权限,链接数,所有者,所属用户组,文件大小,最后修改日期,文件名。

  文件的类型和权限由10个字符组成,第一个字符表示文件的类型,’d‘表示目录,’-‘表示文件,具体如下图所示:



  图释:

-  关于文件类型,这里有一点你必需时刻牢记Linux里面一切皆文件,正因为这一点才有了设备文件( /dev 目录下有各种设备文件,大都跟具体的硬件设备相关)这一说。
-  读权限,表示你可以使用 cat 之类的命令来读取某个文件的内容。
-  写权限,表示你可以编辑和修改某个文件。
-  执行权限,通常指可以运行的二进制程序文件或者脚本文件,如同 Windows 上的 'exe' 后缀的文件。
-  你需要注意的一点是,一个目录要同时具有读权限和执行权限才可以打开,而一个目录要有写权限才允许在其中创建其它文件。

  明白了文件权限的一些概念,我们顺带补充一下关于ls命令的一些其它常用的用法。


  范例7:ls命令。

1
2
3
4
5
6
7
8
9
10
11
# 显示所有隐藏文件(Linux 下以 '.' 开头的文件为隐藏文件,Linux程序(包括Shell)通常使用隐藏文件来保存配置信息)。
ls -A

# 你也可以同时使用A和l参数。
ls -Al

# 显示文件的大小,单位kb。
ls -s

#显示所有文件的大小,并以普通人能看懂的方式呈现。
ls -AsSh

语句解释:
-  大S为按文件大小排序,h用来在文件大小后面加上单位。


  如果你有一个自己的文件不想被其他用户读、写、执行,那么就需要对文件的权限做修改,这里有两种方式。
  范例8:修改文件的访问权限。

1
2
3
4
5
6
7
8
9
# 方式一:二进制数字表示。

# 每个文件都有的三组权限(拥有者,所属用户组,其他用户,记住这个顺序是固定的)。
# 我们用3位二进制数字表示,对于"rwx"可以得到111,也就是一个十进制的'7'。
# 因此“rwx------”对应的数字权限就应该是700。
chmod 700 a.txt


# 方式二:加减赋值操作,自己查去。

语句解释:
-  使用chmod命令来修改文件的权限。
-  你也可以使用chown命令修改文件的拥有者、使用chgrp命令修改所属用户组。

环境变量

  Linux中同样存在环境变量的概念,在介绍环境变量之前,我们先来看看自定义变量。


  范例1:自定义变量。

1
2
3
4
5
# 打开Shell窗口,输入如下命令来定义一个变量,其中declare关键字可以省写。
declare cutler="huye"

# 打印出变量的值,echo用来执行打印操作,在变量名前面加个$符号就可以访问变量值。
echo $cutler

语句解释:
-  自定义变量的作用于仅限于当前Shell窗口,窗口关闭后或者在另一个窗口中,是访问不到cutler变量的。


  自定义变量属于Shell编程的范畴,想了解更多内容的话,请自行搜索。
  Linux的环境变量配置网上也有很多教程(包括PATH变量的配置),笔者就不再冗述了,请自行搜索。

管道

  有时候,我们可以把两个命令连起来使用,一个命令的输出作为另一个命令的输入,这就叫做管道。为了建立管道,需要在两个命令之间使用竖线“|”连接。
  管道是Linux进程之间一种重要的通信机制;除了管道,还有共享内存、消息队列、信号、套接字(socket) 等进程通信机制。


  范例1:使用管道。

1
2
3
4
5
# 列出当前目录下包含“cutler”关键字的文件的信息。
ls -l | grep 'cutler'

# grep支持使用正则去匹配,下面的命令只会匹配最后一个o,因为$表示结尾。
echo "oh, Hello" | grep ".*o$"

语句解释:
-  管道使用竖线(|)将两个命令隔开,竖线左边命令的输出就会作为竖线右边命令的输入。连续使用竖线表示第一个命令的输出会作为第二个命令的输入,第二个命令的输出又会作为第三个命令的输入,依此类推。
-  grep命令有很多选项:
   -  -v 反转查询,输出不匹配的行。
   -  -n 输出匹配的行以及行号。
   -  等等。


  范例2:awk和sort命令。

1
2
3
4
# 首先获取出/etc/passwd文件的内容,然后把结果传给awk命令。
# 接着awk会依次对每一行进行处理,即以‘:’符为分隔符,拆分每一行字符串,并且将该行的第一个值给输出。
# 最后使用sort命令对awk的输出进行排序。
cat /etc/passwd | awk -F ':' '{print $1}' | sort

语句解释:
-  awk和sort命令还支持很多附加参数(比如sort可以按数字的大小排序等),详细情况请自行搜索。



本节参考阅读:

第二节 Linux内核

  Linux内核主要的功能有:进程管理、内存管理、虚拟文件系统、设备控制、网络,每一个功能都涉及到大量的知识,本节只会介绍相关的理论,更深层的原理就需要靠各位自己了。

进程管理

  先来介绍Linux进程管理的知识,作为一个Android系统程序员不懂Linux进程管理,是说不过去的。


进程

  为什么要引入进程?

-  在没有进程的系统中,程序的计算操作和IO操作必须顺序执行,即要么先执行IO操作,要么先执行计算操作,它们不能同时执行。
-  引入进程后,可分别为计算程序和IO程序各建立一个进程,则这两个进程就可以并发执行。多个进程可以相互切换,当失去CPU的进程再次被调度的时候,会根据其PCB中的数据,还原程序现场。

  进程通常由:程序、数据和进程控制块(PCB)组成。

-  程序:就是代码,描述了进程需要完成的功能。
-  数据:程序执行的所需要的数据及工作区。
-  进程控制块:是进程存在的唯一标志。每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

  进程PCB中保存的信息:

-  进程标识符:用于唯一地标识一个进程,一个进程通常有两种标识符。
   -  内部标识符:操作系统分配给进程的唯一的数字ID,即PID(Process ID)。
   -  外部标识符:一个字符串,因为数字不便于记忆,所以每个进程除了数字外,还有一个字符串唯一标识。
-  进程状态:可以是new、ready、running、waiting或 blocked等。
-  进程调度信息:进程状态、进程优先级、进程调度的其他信息。

  其中某些信息是会动态变化的,如进程的运行状态。


父进程 & 子进程

-  在Linux里,每个进程都有一个唯一标识自己的ID,即PID。
-  除了进程0(即PID=0的进程)以外的所有进程都是由其他进程使用系统调用fork创建的。
-  调用fork创建新进程的进程即为父进程,而相对应的,被创建出的进程则为子进程。
-  因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。
-  进程0是系统引导时创建的一个特殊进程,在其调用fork创建出一个子进程(即PID=1的进程1,又称init)后,进程0就转为交换进程(有时也被称为空闲进程),而进程1(init进程)就是系统里其他所有进程的祖先。

  范例1:查看进程树。

1
2
# 以树形结构来显示各个进程的继承关系。
pstree

语句解释:
-  从命令的输出结果可以看出,init是最顶层的进程。


进程的创建

  前面说过,Linux中的所有进程(除了0号进程)都是其父进程调用fork函数创建的。

  除此之外,还有一些知识需要知道:

-  子进程被创建时,除了执行必要的初始化外,系统还需要为它创建PCB以及分配PID。
-  通常情况下,子进程的PID会在父进程的PID上+1,且PID的最大值是32767,当超过了这个上限后,PID就开始循环使用已闲置的小PID号。
-  所谓的创建子进程,其实就是创建一个父进程的副本,把这个副本当做新进程来用,副本会复制父进程内存的内容、线程以及线程执行到的位置等。
-  子进程创建完毕后,子进程和父进程就会各自独立的运行了,父进程会继续执行fork函数的下一条语句,由于子进程是完整复制父进程,所以子进程也会从fork函数的下一句执行。
-  不同的是,父进程调用fork函数后,得到的返回值是子进程的PID,而子进程从fork函数中得到的返回值却是0。


  范例1:查看进程ID的取值范围。

1
cat /proc/sys/kernel/pid_max


查看进程信息

  在Linux中最常用的进程管理命令莫过于ps和top了,它们二者都是用来查看进程信息的,但不同的是前者是静态的,后者会定时更新显示。



图释:
-  上图显示的内容是top命令输出的。
-  这两个命令的具体用法网上有很多,这里就不再冗述了,简单的说一下各列的含义为:
   -  第一列是进程id,第二列是进程的所有者,PR是进程优先级,NI是进程NICE值。
   -  S是进程的状态,%CPU是上次更新到现在的CPU时间占用百分比,COMMAND是进程的名称。

  接下来会详细介绍上图中各列的含义。


进程的优先级

  Linux与其他系统一样,需要在多个进程之间共享CPU等资源,若某个进程占用了100%的CPU,那么其他进程将无法响应。

-  如果运行的进程数大于CPU的数量,那么就得保证每个进程都能使用到CPU。
-  通常的做法是,选择一个要执行的进程,并让它在短时间内运行(这个时间称为时间片),或者一直运行到它等待的事件(如IO)完成。
-  选择哪个进程是有策略的,为了确保重要的进程能够得到CPU,这种选择是基于进程的优先级的。


  在Linux中,进程总共有140个优先级,按照等级可将进程划分为:实时进程和普通进程。



图释:
-  数字越小,优先级越大,即0是最高优先级。
-  实时进程默认使用0~99之间的优先级,普通进程则默认使用100~139之间的优先级。
-  实时进程专门执行一些非常重要的任务,它要求系统必须尽快安排CPU等资源响应请求,特别在生产制造控制和军事领域。
-  PR(priority)表示进程的优先级,它会受到Nice值(范围是-20~19)的影响,即PRI(new)=PRI(old)+NI。
-  “Nice值”这个名称来自英文单词nice,意思为友好;Nice值越高,这个进程优先级也就越低,且越“友好、谦让”,就会让给其他进程越多的时间。
-  在通常情况下,子进程会继承父进程的nice值,由于init进程会被赋予0,所以其子进程也是0。


时间片

  现代操作系统(如:Windows、Linux、Mac OS X等)允许同时运行多个进程(在听音乐的同时浏览网页)。

-  通常系统中会同时运行好几十个进程,但PC是不可能同时装几十个CPU的。
-  所以系统中的所有进程需要排队,依次去请求使用CPU,系统依据一定的策略,选中某个进程,然后让它使用一段时间CPU,这段时间就是时间片。
-  这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。

  通常状况下,一个系统中所有的进程被分配到的时间片长短并不是相等的。

-  系统通过测量进程处于“睡眠”和“正在运行”状态的时间长短来计算每个进程的交互性。
-  交互性和每个进程预设的静态优先级(Nice值)的叠加即是动态优先级。
-  动态优先级按比例缩放就是要分配给那个进程时间片的长短。
-  一般地,为了获得较快的响应速度,交互性强的进程(即趋向于IO消耗型)被分配到的时间片要长于交互性弱的(趋向于处理器消耗型)进程。


进程的状态

  进程常见有如下几个状态:

-  R:正在运行或者处于就绪状态(已经被加入到运行队列等待CPU中),同一时刻可能有多个进程处于此状态。
-  S:可中断的睡眠状态(interruptible sleep),处于这个状态的进程因为等待某个事件的发生(比如等待socket连接、等待信号量)而被挂起。一般情况下,绝大多数进程都处于这个状态。
-  D:不可终端的睡眠状态(uninterruptible sleep ),通常是在IO操作中(磁盘IO,网络IO等)。
-  Z:僵尸进程。


  僵尸进程 & 孤儿进程:

-  当一个子进程结束运行(一般是调用exit、运行时发生致命错误或收到终止信号所导致)时,子进程的退出状态(返回值)会回报给操作系统,系统则以SIGCHLD信号将子进程被结束的事件告知父进程,此时子进程的进程控制块(PCB)仍驻留在内存中。一般来说,收到SIGCHLD后,父进程会使用wait系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的PCB;而如若父进程没有这么做的话,子进程的PCB就会一直驻留在内存中,也即成为僵尸进程。
-  孤儿进程则是指父进程结束后仍在运行的子进程。在类UNIX系统中,孤儿进程一般会被init进程所“收养”,成为init的子进程。
-  因此解决僵尸进程的问题,可以通过杀死父进程来让僵尸成为孤儿,然后被init收养,最后被超度,极乐世界。



本节参考阅读:

内存管理


问题起源

  特此声明:本小节主要参考自《深入理解Linux中内存管理》,为了防止遗失,所以将其转载过来,有改动。


  我们先来回顾一下历史:

-  在早期的计算机中,程序是直接运行在物理内存上的,即程序在运行的过程中访问的都是物理地址。
-  若系统同一时间只能运行一个程序,那么只要这个程序所需的内存不超过该机器的物理内存就不会出现问题,也就不需要考虑内存管理的事了。
-  然而现在的系统都是支持多任务,多进程的,因为这样CPU以及其他硬件的利用率会更高,这个时候我们就要考虑到将系统内有限的物理内存如何及时有效的分配给多个程序了,这个事情本身我们就称之为内存管理。

  这里举一个早期的计算机系统中,内存分配管理的例子,以便于大家理解:

-  假如我们有A,B,C三个程序,它们运行时分别需要10M、100M、20M内存。
-  如果此时系统需要同时运行程序A和B,那么早期的内存管理过程会先将物理内存的前10M分配给A,接下来的10M-110M分配给B,这种内存管理的方法比较直接。
-  那么我们这个时候想让程序C也运行,同时假设我们系统的内存只有128M,显然按照这种方法程序C由于内存不够是不能够运行的。
-  解决方法就是交换,即当内存空间不够的时,可以将程序不需要用到的数据交换到磁盘空间上去,以达到扩展内存空间的目的。


  虽然找到了解决方案,但这种内存管理方案还是存在的三个比较明显的问题:


  1、进程地址空间不能隔离

-  由于程序直接访问的是物理内存,这个时候程序所使用的内存空间不是隔离的。
-  举个例子,就像上面说的A的地址空间是0-10M这个范围内,但是如果A中有一段代码是操作10M-128M这段地址空间内的数据,那么程序B和程序C就很可能会崩溃(每个程序都可以访问系统的整个地址空间)。
-  这样很多恶意程序或者是木马程序可以轻而易举地破快其他的程序,系统的安全性也就得不到保障了,这对用户来说也是不能容忍的。


  2、内存使用的效率低

-  上面说过,如果想让程序A、B、C同时运行,就得使用交换技术将程序暂时不用的数据写到磁盘上,在需要的时再读回内存。
-  很显然,当程序C要运行时,不能将A交换到磁盘上去,因为程序是需要连续的地址空间的,程序C需要20M的内存,而A只有10M的空间。
-  那只有将B换出去了,因为B有100M空间。
-  也就是说,为了运行C我们需要将100M的数据从内存写到磁盘,然后在程序B需要运行时再从读回内存。
-  我们知道IO操作比较耗时,所以这个过程效率将会十分低下。


  3、程序运行的地址不能确定

-  程序每次需要运行时,都需要在内存中分配一块足够大的空闲区域,而问题是这个空闲的位置是不能确定的。
-  假设有A、B、C三个任务,A和C都是10M,B是110M,且总内存为128M。
-  先运行A和C,当B需要执行时,就会把C给拿出来,稍后当C需要执行时,系统就会把A拿出来,把C放到A的位置。
-  由于在这种模式下,程序操作的都是物理地址(即绝对地址),那么当C被放到A的位置上时,就会有问题了,就需要执行地址重定向的操作。


  内存管理无非就是想办法解决上面三个问题。

  这里引用一句不客观的名言:“计算机系统里的任何问题都可以靠引入一个中间层来解决”。


分段虚拟内存

-  为了解决上面的问题,最初是在程序和物理内存之间引入了虚拟内存的概念。
-  虚拟内存位于程序和物理内存之间,程序只能看见虚拟内存,再也不能直接访问物理内存。
-  每个程序都有自己独立的进程地址空间,这样就做到了进程隔离。


  下图是分段虚拟内存的示例:



图释:
-  在分段技术中,每个程序都有其独立的虚拟的进程地址空间,上图程序A和B的虚拟地址空间都是从0x00000000开始的。
-  即每个进程只需要记住自己的起始地址即可。地址映射的时候,由进程内的相对地址加上起始地址就可以得到物理地址,这个映射过程由硬件来完成。
-  分段机制解决了前面提到的进程地址空间隔离(进程访问不属于自身的地址时,内核将拒绝)和程序地址重定位的问题。


  但是分段技术仍然是以程序为单位,当内存不足时仍然需要将整个程序交换到磁盘,这样内存使用的效率仍然很低。


分页虚拟内存

  根据程序的局部性运行原理,一个程序在运行的过程当中,在某个时间段内,只有一小部分数据会被经常用到,所以我们需要更加小粒度的内存分割和映射方法。

  因此,另一种将虚拟地址转换为物理地址的方法,分页机制应运而生了。

-  分页机制在分段机制的基础上做了增强,它把内存空间分为若干个很小的固定大小的页,且以页为单位进行管理。
-  也就是说,在内存分配时,每个进程都被会分配整数个页的内存,而不会包含小数(比如100.4)个页。
-  这样的话,每一页的大小就不应该太大,若设置每页为1M的话,且进程的最后一页只用1k,那就浪费了999k。
-  Linux中一般页的大小是4KB。


图释:
-  上图可以看到进程1(左)和进程2(右)的虚拟地址空间都被映射到了不连续的地址空间内(这意味着页与页之间是不需要紧挨着的的)。
-  进程1的虚拟页VP2和VP3被交换到了磁盘中,在程序需要这两页的时候,Linux内核会产生一个缺页异常,然后异常管理程序会将其读到内存中。


  事实上,Linux基本不使用分段的机制:

-  或者说Linux中的分段机制只是为了兼容IA32的硬件而设计的。
-  Linux内核使用页式内存管理,应用程序给出的内存地址是虚拟地址,它需要经过若干级页表一级一级的变换,才变成真正的物理地址。


  分页机制解决了“内存使用的效率低”的问题,但是在整个过程中,还有几个知识点要说一下。


MMU

  MMU是 Memory Management Unit 的缩写,负责虚拟地址映射为物理地址。

-  当程序访问一个由虚拟地址表示的内存空间时,需要先经过若干次的内存访问,得到每一级页表中用于转换的页表项(页表是存放在内存里面的),才能完成映射。
-  也就是说,要实现一次内存访问,实际上内存被访问了N+1次(N=页表级数),并且还需要做N次加法运算。
-  所以,地址映射必须要有硬件支持,MMU(内存管理单元)就是这个硬件。
-  并且需要有cache来保存页表,这个cache就是TLB(Translation lookaside buffer),尽管如此,地址映射还是有着不小的开销。
-  假设cache的访存速度是内存的10倍,命中率是40%,页表有三级,那么平均一次虚拟地址访问大概就消耗了两次物理内存访问的时间。


交换技术

  将进程的内存换入换出到磁盘上的技术,就是swap技术:

-  当物理内存不够用了,而又有新的程序请求分配内存,系统就会将其他程序暂时不用的数据交换到物理磁盘上(swap out),等需要再读入(swap in)。
-  这样做的坏处显而易见,换入换出的代价比较大,相比数据一直放在内存里面,多了读磁盘的操作,而磁盘IO代价。
-  硬盘中的用作交换的部分被称为交换空间(Swap Space),通常情况下,Swap空间应大于或等于物理内存的大小,是物理内存的2-2.5倍。
-  如果是小的桌面系统,则只需要较小的Swap空间,而大的服务器系统则视情况不同需要不同大小。


OOM Killer

  Linux内存管理模块有一个 OverCommit 机制,意思是说,进程申请的内存可以大于当前系统free的内存。

-  这是因为进程申请的内存不会马上就被用到,并且在进程的整个生命周期内,它很大可能是不会用完它申请的所有内存。
-  进程申请的内存可以大于当前系统free的内存是可以在有限的物理内存上,为更多的进程服务。
-  用小区宽带的例子来讲可能更容易懂一些,商家自己只有100M的带宽,正常情况下每人买10M,他只能卖给10个人,但是由于每个人并不会24小时都用满自己的10M带宽,所以商家把100M的带宽卖给了13个人也不会有问题。


  但是与此同时也带来了一个风险:

-  假设进程A最初申请了100M内存,但只用了20M,由于overcommit机制的存在,它剩余的80M被系统暂借给别的进程了。
-  当它需要使用自己剩余的80M内存时,却因为系统中开满了进程,而导致系统也没有内存还给进程A了。
-  在这种情况下,进程A甚至连一个page的内存都无法申请,于是oom killer就出现了。
-  它会识别出来可以为整个系统作出牺牲的进程,然后杀掉它,释放出来一些内存。


  提示:

-  在Linux的内存分配机制中,进程被关闭后,其所占用的内存不会被立刻回收。
-  而是用来做缓存使用,这样当该进程再次被开启时速度会变快。但当系统内存不足时,OOM Killer肯定是优先杀死缓存进程的。


kswapd0

  在Linux中有一个名为kswapd0的进程:

-  它是虚拟内存管理中负责换页的,操作系统每过一定时间就会唤醒kswapd,看看内存是否紧张,如果不紧张则睡眠。
-  在kswapd中有2个阀值,pages_hige和pages_low,当空闲内存页的数量低于pages_low的时候,kswapd进程就会扫描内存并且每次释放出32个free pages,直到free page的数量到达pages_high。
-  也就是说,当kswapd进程占据大量cpu资源时,就意味着当前系统的内存开始不足了。



本节参考阅读: