liunx 随笔(持续更新本文)
ps -ef
UID 指进程的所有者;
PID 是进程的唯一标识;
PPID 是进程的父进程 ID;
C 是 CPU 的利用率(就是 CPU 占用);
STIME 是开始时间;
TTY 是进程所在的 TTY,如果没有 TTY 就是 ?号;
TIME;
CMD 是进程启动时的命令,如果不是一个 Shell 命令,而是用方括号括起来,那就是系统进程或者内核过程。
==========================================================================
输入输出流
具体来说>符号叫作覆盖重定向;>>叫作追加重定向。>每次都会把目标文件覆盖,>>会在目标文件中追加。比如你每次启动一个程序日志都写入/var/log/somelogfile中,可以这样操作,如下所示:
复制代码
start.sh >> /var/log/somelogfile
经过这样的操作后,每次执行程序日志就不会被覆盖了。
另外还有一种情况,比如我们输入:
复制代码
ls1 > out
结果并不会存入out文件,因为ls1指令是不存在的。结果会输出到标准错误流中,仍然在屏幕上。这里我们可以把标准错误流也重定向到标准输出流,然后再重定向到文件。
复制代码
ls1 &> out
这个写法等价于:
复制代码
ls1 > out 2>&1
相当于把ls1的标准输出流重定向到out,因为ls1 > out出错了,所以标准错误流被定向到了标准输出流。&代表一种引用关系,具体代表的是ls1 >out的标准输出流。
================================================================================
管道
管道和重定向很像,但是管道是一个连接一个进行计算,重定向是将一个文件的内容定向到另一个文件,这二者经常会结合使用。
接下来我们以多个场景举例帮助你深入学习管道。
排序
ls |sort -r
去重
sort a.txt |uniq
筛选
find ./ | grep Spring
中间结果
管道一个接着一个,是一个计算逻辑。有时候我们想要把中间的结果保存下来,这就需要用到tee指令。tee指令从标准输入流中读取数据到标准输出流。
这时候,你可能会问: 老师, 这不是什么都没做吗?
别急,tee还有一个能力,就是自己利用这个过程把输入流中读取到的数据存到文件中。比如下面这条指令:
复制代码
find ./ -iname "*.java" | tee JavaList | grep Spring
=======================================================================================
xargs
举个例子,如果我们重命名当前目录下的所有 .a 的文件,想在这些文件前面加一个前缀prefix_。比如说x.a文件需要重命名成prefix_x.a,我们就可以用xargs指令构造模块化的指令。
ls |xargs -I MM echo "mv MM prefix_MM"
现在我们有x.a``y.a``z.a三个文件,如下图所示:
ls |xargs -I MM mv MM prefix_MM
xargs 的作用了吗?
xargs 将标准输入流中的字符串分割成一条条子字符串,然后再按照我们自己想要的方式构建成一条条指令,大大拓展了 Linux 指令的能力。
===========================================================
===内核===
内核提供操作硬件、磁盘、内存分页、进程等最核心的能力,并拥有直接操作全部内存的权限,因此内核不能把自己的全部能力都提供给用户,而且也不能允许用户通过shell指令进行系统调用。Linux 下内核把部分进程需要的系统调用以 C 语言 API 的形式提供出来。部分系统调用会有权限检查,比如说设置系统时间的系统调用。
以上我们看到了 Linux 对系统权限的抽象。接下来我们再说说权限架构的思想。
权限架构思想
优秀的权限架构主要目标是让系统安全、稳定且用户、程序之间相互制约、相互隔离。这要求权限系统中的权限划分足够清晰,分配权限的成本足够低。
因此,优秀的架构,应该遵循最小权限原则(Least Privilege)。权限设计需要保证系统的安全和稳定。比如:每一个成员拥有的权限应该足够的小,每一段特权程序执行的过程应该足够的短。对于安全级别较高的时候,还需要成员权限互相牵制。比如金融领域通常登录线上数据库需要两次登录,也就是需要两个密码,分别掌握在两个角色手中。这样即便一个成员出了问题,也可以保证整个系统安全。
同样的,每个程序也应该减少权限,比如说只拥有少量的目录读写权限,只可以进行少量的系统调用。
权限划分
此外,权限架构思想还应遵循一个原则,权限划分边界应该足够清晰,尽量做到相互隔离。Linux 提供了用户和分组。当然 Linux 没有强迫你如何划分权限,这是为了应对更多的场景。通常我们服务器上重要的应用,会由不同的账户执行。比如说 Nginx、Web 服务器、数据库不会执行在一个账户下。现在随着容器化技术的发展,我们甚至希望每个应用独享一个虚拟的空间,就好像运行在一个单独的操作系统中一样,让它们互相不用干扰。
到这里,你可能会问:为什么不用 root 账户执行程序? 下面我们就来说说 root 的危害。
举个例子,你有一个 Mysql 进程执行在 root(最大权限)账户上,如果有黑客攻破了你的 Mysql 服务,获得了在 Mysql 上执行 Sql 的权限,那么,你的整个系统就都暴露在黑客眼前了。这会导致非常严重的后果。
黑客可以利用 Mysql 的 Copy From Prgram 指令为所欲为,比如先备份你的关键文件,然后再删除他们,并要挟你通过指定账户打款。如果执行最小权限原则,那么黑客即便攻破我们的 Mysql 服务,他也只能获得最小的权限。当然,黑客拿到 Mysql 权限也是非常可怕的,但是相比拿到所有权限,这个损失就小多了。
分级保护
因为内核可以直接操作内存和 CPU,因此非常危险。驱动程序可以直接控制摄像头、显示屏等核心设备,也需要采取安全措施,比如防止恶意应用开启摄像头盗用隐私。通常操作系统都采取一种环状的保护模式。
如上图所示,内核在最里面,也就是 Ring 0。 应用在最外面也就是Ring 3。驱动在中间,也就是 Ring 1 和 Ring 2。对于相邻的两个 Ring,内层 Ring 会拥有较高的权限,可以改变外层的 Ring;而外层的 Ring 想要使用内层 Ring 的资源时,会有专门的程序(或者硬件)进行保护。
比如说一个 Ring3 的应用需要使用内核,就需要发送一个系统调用给内核。这个系统调用会由内核进行验证,比如验证用户有没有足够的权限,以及这个行为是否安全等等。
权限包围(Privilege Bracking)
之前我们讨论过,当 Mysql 跑在 root 权限时,如果 Mysql 被攻破,整个机器就被攻破了。因此我们所有应用都不要跑在 root 上。如果所有应用都跑在普通账户下,那么就会有临时提升权限的场景。比如说安装程序可能需要临时拥有管理员权限,将应用装到/usr/bin目录下。
Linux 提供了权限包围的能力。比如一个应用,临时需要高级权限,可以利用交互界面(比如让用户输入 root 账户密码)验证身份,然后执行需要高级权限的操作,然后马上恢复到普通权限工作。这样做可以减少应用在高级权限的时间,并做到专权专用,防止被恶意程序利用。
===========================================================
为用户增加次级分组
组分成主要分组(Primary Group)和次级分组(Secondary Group)。主要分组只有 1 个,次级分组可以有多个。如果想为用户添加一个次级分组,可以用usermod指令。下面指令将用户foo添加到sudo分组,从而foo拥有了sudo的权限。
sudo usermod -a -G sudo foo
-a代表append,-G代表一个次级分组的清单, 最后一个foo是账户名。
修改用户主要分组
修改主要分组还是使用usermod指令。只不过参数是小写的-g。
sudo usermod -g somegroup foo
====================================================================
DNS 查询
我们排查网络故障时想要进行一次 DNS Lookup,想知道一个网址 DNS 的解析过程。这个时候有多个指令可以用。
host
host 就是一个 DNS 查询工具。比如我们查询拉勾网的 DNS,如下图所示:
host www.ipieuvre.com
dig
dig指令也是一个做 DNS 查询的。不过dig指令显示的内容更详细。下图是dig拉勾网的结果。
dig www.ipieuvre.com
curl还可以执行 POST 请求,比如下面这个语句:
curl -d '{"x" : 1}' -H "Content-Type: application/json" -X POST http://localhost:3000/api
curl在向localhost:3000发送 POST 请求。-d后面跟着要发送的数据, -X后面是用到的 HTTP 方法,-H是指定自定义的请求头。
如何查看正在 TIME_WAIT 状态的连接数量?
netstat -an | grep TIME_WAIT | wc -l
===========================================================================
包管理器使用
Linux 下的应用程序多数以软件包的形式发布,用户拿到对应的包之后,使用包管理器进行安装。说到包管理器,就要提到dpkg和rpm。
我们先说说包。 Linux 下两大主流的包就是rpm和dpkg。
dpkg(debian package),是linux一个主流的社区分支开发出来的。社区就是开源社区,有很多世界顶级的程序员会在社区贡献代码,比如 github。一般衍生于debian的 Linux 版本都支持dpkg,比如ubuntu。
rpm(redhatpackage manager)。在正式讲解之前,我们先来聊聊 RedHat 这家公司。
RedHat 是一个做 Linux 的公司,你可以把它理解成一家“保险公司”。 很多公司购买红帽的服务,是为了给自己的业务上一个保险。以防万一哪天公司内部搞不定 Linux 底层,或者底层有 Bug,再或者底层不适合当下的业务发展,需要修改等问题,红帽的工程师都可以帮企业解决。
再比如,RedHat 收购了JBoss,把 JBoss 改名为 WildFly。 像 WildFly 这种工具更多是面向企业级,比如没有大量研发团队的企业会更倾向使用成熟的技术。RedHat 公司也有自己的 Linux,就叫作 RedHat。RedHat 系比较重要的 Linux 有 RedHat/Fedora 等。
无论是dpkg还是rpm都抽象了自己的包格式,就是以.dpkg或者.rpm结尾的文件。
dpkg和rpm也都提供了类似的能力:
查询是否已经安装了某个软件包;
查询目前安装了什么软件包;
给定一个软件包,进行安装;
删除一个安装好的软件包。
关于dpkg和rpm的具体用法,你可以用man进行学习。接下来我们聊聊yum和apt。
自动依赖管理
Linux 是一个开源生态,因此工具非常多。工具在给用户使用之前,需要先打成dpkg或者rpm包。 有的时候一个包会依赖很多其他的包,而dpkg和rpm不会对这种情况进行管理,有时候为了装一个包需要先装十几个依赖的包,过程非常艰辛!因此现在多数情况都在用yum和apt。
yum
你可能会说,我不用yum也不用apt,我只用docker。首先给你一个连击 666,然后我还是要告诉你,如果你做docker镜像,那么还是要用到yum和apt,因此还是有必要学一下。
yum的全名是 Yellodog Updator,Modified。 看名字就知道它是基于Yellodog Updator这款软件修改而来的一个工具。yum是 Python 开发的,提供的是rpm包,因此只有redhat系的 Linux,比如 Fedora,Centos 支持yum。yum的主要能力就是帮你解决下载和依 赖两个问题。
下载之所以是问题,是因为 Linux 生态非常庞大,有时候用户不知道该去哪里下载一款工具。比如用户想安装vim,只需要输入sudo yum install vim就可以安装了。yum的服务器收集了很多linux软件,因此yum会帮助用户找到vim的包。
另一方面,yum帮助用户解决了很多依赖,比如用户安装一个软件依赖了 10 个其他的软件,yum会把这 11 个软件一次性的装好。
apt全名是 Advanced Packaging Tools,是一个debian及其衍生 Linux 系统下的包管理器。由于advanced(先进)是相对于dpkg而言的,因此它也能够提供和yum类似的下载和依赖管理能力。比如在没有vim的机器上,我们可以用下面的指令安装vim。如
sudo apt install vim
dpkg -l vim
然后用dpkg指令查看 vim 的状态是ii。第一个i代表期望状态是已安装,第二个i代表实际状态是已安装。
sudo apt remove vim
dpkg -l vim
我们看到 vim 的状态从ii变成了rc,r是期望删除,c是实际上还有配置文件遗留。 如果我们想彻底删除配置文件,可以使用apt purge,就是彻底清除的意思,
sudo apt purge vim
期待结果是u就是 unkonw(未知)说明已经没有了。实际结果是n,就是 not-installed(未安装)。
如果想查询mysql相关的包,可以使用apt serach mysql,这样会看到很多和mysql相关的包,
如果我们想精确查找一个叫作mysql-server的包,可以用apt list。
你可以使用sudo lsb_release -a查看自己的 Ubuntu 版本。
====编译安装和包管理安装有什么优势和劣势了吗?=====
第一点是需要提前将包编译好,因此有一个发布的过程,如果某个包没有发布版本,或者在某个平台上找不到对应的发布版本,就需要编译安装。
第二点就是如果一个软件的定制程度很高,可能会在编译阶段传入参数,比如利用configure传入配置参数,这种时候就需要编译安装。
========================================================
在分析日志前,给你提个醒,记得要less一下,看看日志里面的内容。之前我们说过,尽量使用less这种不需要读取全部文件的指令,因为在线上执行cat是一件非常危险的事情,这可能导致线上服务器资源不足。
PV(Page View),用户每访问一个页面就是一次Page View。对于nginx的acess_log来说,分析 PV 非常简单,我们直接使用wc -l就可以看到整体的PV。
wc -l access.log
===PV 分组===
awk '{print $4}' access.log | less
我们想要按天统计,可以利用 awk提供的字符串截取的能力。
awk '{print substr($4, 2, 11)}' train_ipieuvre_com_mange.com_log
我们使用awk的substr函数,数字2代表从第 2 个字符开始,数字11代表截取 11 个字符。
接下来我们就可以分组统计每天的日志条数了。
awk '{print substr($1, 1, 10)}' train_ipieuvre_com_mange.com_log |sort |uniq -c
使用sort进行排序,然后使用uniq -c进行统计。你可以看到从 2015 年 5 月 17 号一直到 6 月 4 号的日志,还可以看到每天的 PV 量大概是在 2000~3000 之间。
===分析 UV===
接下来我们分析UV。UV(UniqVisitor),也就是统计访问人数。通常确定用户的身份是一个复杂的事情,但是我们可以用 IP 访问来近似统计 UV。
awk '{print $1}' train_ipieuvre_com_mange.com_log |sort|uniq | wc -l
我们使用 awk 去打印$1也就是第一列,接着sort排序,然后用uniq去重,最后用wc -l查看条数。 这样我们就知道日志文件中一共有2660个 IP,也就是2660个 UV。
===分组分析 UV===
接下来我们尝试按天分组分析每天的 UV 情况。这个情况比较复杂,需要较多的指令,我们先创建一个叫作sum.sh的bash脚本文件,写入如下内容
#!/usr/bin/bash
awk '{print substr($4, 2, 11) " " $1}' access.log |\
sort | uniq |\
awk '{uv[$1]++;next}END{for (ip in uv) print ip, uv[ip]}'
具体分析如下。
文件首部我们使用#!,表示我们将使用后面的/usr/bin/bash执行这个文件。
第一次awk我们将第 4 列的日期和第 1 列的ip地址拼接在一起。
下面的sort是把整个文件进行一次字典序排序,相当于先根据日期排序,再根据 IP 排序。
接下来我们用uniq去重,日期 +IP 相同的行就只保留一个。
最后的awk我们再根据第 1 列的时间和第 2 列的 IP 进行统计。
为了理解最后这一行描述,我们先来简单了解下awk的原理。
awk本身是逐行进行处理的。因此我们的next关键字是提醒awk跳转到下一行输入。 对每一行输入,awk会根据第 1 列的字符串(也就是日期)进行累加。之后的END关键字代表一个触发器,就是 END 后面用 {} 括起来的语句会在所有输入都处理完之后执行——当所有输入都执行完,结果被累加到uv中后,通过foreach遍历uv中所有的key,去打印ip和ip对应的数量。
根据今天的 access_log 分析出有哪些终端访问了这个网站,并给出分组统计结果。
根据今天的 access_log 分析出访问量 Top 前三的网页。
awk -F\" '{print $6}' access.log | sort | uniq -c | sort -fr2.
awk '{print $7}' access.log | sort | uniq -c | sort -fr | head -3
=====================================================
利用 Linux 指令同时在多台机器部署程序
#!/usr/bin/bash
readarray -t ips < iplist
for ip in ${ips[@]}
do
echo $ip
done
首行的#!叫作 Shebang。Linux 的程序加载器会分析 Shebang 的内容,决定执行脚本的程序。这里我们希望用bash来执行这段程序,因为我们用到的 readarray 指令是bash 4.0后才增加的能力。
readarray指令将 iplist 文件中的每一行读取到变量ips中。ips是一个数组,可以用echo ${ips[@]}打印其中全部的内容:@代表取数组中的全部内容;$符号是一个求值符号。不带$的话,ips[@]会被认为是一个字符串,而不是表达式。
for循环遍历数组中的每个ip地址,echo把地址打印到屏幕上。
如果用shell执行上面的程序会报错,因为readarray是bash 4.0后支持的能力,因此我们用chomd为foreach.sh增加执行权限,然后直接利用shebang的能力用bash执行,
创建集群管理账户
create_lagou.sh:
sudo useradd -m -d /home/lagou lagou
sudo passwd lagou
sudo usermod -G sudo lagou
sudo usermod --shell /bin/bash lagou
sudo cp ~/.bashrc /home/lagou/
sudo chown lagou.lagou /home/lagou/.bashrc
sduo sh -c 'echo "lagou ALL=(ALL) NOPASSWD:ALL">>/etc/sudoers'
但是你不要忘记,我们的目标是让程序在成百上千台机器上传播,因此还需要一个脚本将create_lagou.sh拷贝到需要执行的机器上去。
foreach.sh
#!/usr/bin/bash
readarray -t ips < iplist
for ip in ${ips[@]}
do
scp ~/remote/create_lagou.sh ramroll@$ip:~/create_lagou.sh
done
这里,我们在循环中用scp进行文件拷贝,然后分别去每台机器上执行create_lagou.sh。
如果你的机器非常多,上述过程会变得非常烦琐。你可以先带着这个问题学习下面的Step 4,然后再返回来重新思考这个问题,当然你也可以远程执行脚本。另外,还有一个叫作sshpass的工具,可以帮你把密码传递给要远程执行的指令,如果你对这块内容感兴趣,可以自己研究下这个工具。
======= 打通集群权限==========
接下来我们需要打通从主服务器到v1和v2的权限。当然也可以每次都用ssh输入用户名密码的方式登录,但这并不是长久之计。 如果我们有成百上千台服务器,输入用户名密码就成为一件繁重的工作。
这时候,你可以考虑利用主服务器的公钥在各个服务器间登录,避免输入密码。接下来我们聊聊具体的操作步骤:
首先,需要在u1上用ssh-keygen生成一个公私钥对,然后把公钥写入需要管理的每一台机器的authorized_keys文件中。如下图所示:我们使用ssh-keygen在主服务器u1中生成公私钥对。
可以看到id_rsa.pub文件中是加密的字符串,我们可以把这些字符串拷贝到其他机器对应用户的~/.ssh/authorized_keys文件中,当ssh登录其他机器的时候,就不用重新输入密码了。 这个传播公钥的能力,可以用一个shell脚本执行,这里我用transfer_key.sh实现。
我们修改一下foreach.sh,并写一个transfer_key.sh配合foreach.sh的工作。transfer_key.sh内容如下:
==foreach.sh==
#!/usr/bin/bash
readarray -t ips < iplist
for ip in ${ips[@]}
do
sh ./transfer_key.sh $ip
done
==tranfer_key.sh==
ip=$1
pubkey=$(cat ~/.ssh/id_rsa.pub)
echo "execute on .. $ip"
ssh lagou@$ip "
mkdir -p ~/.ssh
echo $pubkey >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
"
在foreach.sh中我们执行 transfer_key.sh,并且将 IP 地址通过参数传递过去。在 transfer_key.sh 中,用$1读出 IP 地址参数, 再将公钥写入变量pubkey,然后登录到对应的服务器,执行多行指令。用mkdir指令检查.ssh目录,如不存在就创建这个目录。最后我们将公钥追加写入目标机器的~/.ssh/authorized_keys中。
将JAVA_HOME加入bash_profile,这样后续远程执行java指令时就可以使用JAVA_HOME环境变量了。
最后,我们将上面所有的指令整理起来,形成一个install_java.sh。
sudo apt -y install openjdk-11-jdk
sudo useradd -m -d /opt/ujava ujava
sudo usermod --shell /bin/bash ujava
sudo sh -c 'echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/" >> /opt/ujava/.bash_profile'
第六步:远程安装 Java 环境
终于到了远程安装 Java 环境这一步,我们又需要用到foreach.sh。为了避免每次修改,你可以考虑允许foreach.sh带一个文件参数,指定需要远程执行的脚本。
foreach.sh
#!/usr/bin/bash
readarray -t ips < iplist
script=$1
for ip in ${ips[@]}
do
ssh $ip 'bash -s' < $script
done
改写后的foreach会读取第一个执行参数作为远程执行的脚本文件。 而bash -s会提示使用标准输入流作为命令的输入;< $script负责将脚本文件内容重定向到远程bash的标准输入流。
然后我们执行foreach.sh install_java.sh,机器等待 1 分钟左右,在执行结束后,可以用下面这个脚本检测两个机器中的安装情况。
sudo -u ujava -i /bin/bash -c 'echo $JAVA_HOME'
sudo -u ujava -i java --version
check.sh中我们切换到ujava用户去检查JAVA_HOME环境变量和 Java 版本。执行的结果如下图所示:
====================================================
====================================================
============ 操作系统基础知识 =================
====================================================
====================================================
操作系统内核:Linux 内核和 Windows 内核有什么区别?
Windows 和 Linux 是当今两款最主流的服务器操作系统产品,都拥有广泛的用户和信徒。Windows 通过强大的商业运作,驱动了大量优秀人才加盟到它的开发团队中;Linux 通过社区产品的魅力吸引着世界上大量的顶级程序员为它贡献源代码、解答问题。两者在服务器市场上竞争激烈,不分伯仲,但也存在互相扶持的关系。
什么是内核?
说到操作系统,就必须说内核。内核是操作系统中应用连接硬件设备的桥梁。
内核的能力
对于一个现代的操作系统来说,它的内核至少应该提供以下 4 种基本能力:
管理进程、线程(决定哪个进程、线程使用 CPU);
管理内存(决定内存用来做什么);
连接硬件设备(为进程、和设备间提供通信能力);
提供系统调用(接收进程发送来的系统调用);
操作系统分层
从上面 4 种能力来看操作系统和内核之间的关系,通常可以把操作系统分成 3 层,最底层的硬件设备抽象、中间的内核和最上层的应用。
Linux 的设计
Linux 操作系统第一版是1991 年林纳斯托·瓦兹(一个芬兰的小伙子,当时 22 岁)用 C 语音写的。 写完之后他在网络上发布了 Linux 内核的源代码。又经过了 3 年的努力,在 1994 年发布了完整的核心 Version 1.0。
Multitask and SMP(Symmetric multiprocessing)
MultiTask 指多任务,Linux 是一个多任务的操作系统。多任务就是多个任务可以同时执行,这里的“同时”并不是要求并发,而是在一段时间内可以执行多个任务。当然 Linux 支持并发。
SMP 指对称多处理。其实是说 Linux下每个处理器的地位是相等的,内存对多个处理器来说是共享的,每个处理器都可以访问完整的内存和硬件资源。 这个特点决定了在Linux上不会存在一个特定的处理器处理用户程序或者内核程序,它们可以被分配到任何一个处理器上执行。
ELF(Executable and Linkable Format)
这个名词翻译过来叫作可执行文件链接格式。这是一种从 Unix 继承而来的可执行文件的存储格式。我们可以看到 ELF 中把文件分成了一个个分段(Segment),每个段都有自己的作用。如果想要深入了解这块知识,会涉及部分编译原理的知识,如果你感兴趣可以去网上多查些资料或者去留言区我们一起讨论。
Monolithic Kernel
这个名词翻译过来就是宏内核,宏内核反义词就是 Microkernel ,微内核的意思。Linux 是宏内核架构,这说明 Linux 的内核是一个完整的可执行程序,且内核用最高权限来运行。宏内核的特点就是有很多程序会打包在内核中,比如,文件系统、驱动、内存管理等。当然这并不是说,每次安装驱动都需要重新编译内核,现在 Linux 也可以动态加载内核模块。所以哪些模块在内核层,哪些模块在用户层,这是一种系统层的拆分,并不是很强的物理隔离。
与宏内核对应,接下来说说微内核,内核只保留最基本的能力。比如进程调度、虚拟内存、中断。多数应用,甚至包括驱动程序、文件系统,是在用户空间管理的。
在内核层和在用户层有什么区别吗?
感觉分层其实差不多。 我这里说一个很大的区别,比如说驱动程序是需要频繁调用底层能力的,如果在内核中,性能肯定会好很多。对于微内核设计,驱动在内核外,驱动和硬件设备交互就需要频繁做内核态的切换。
当然微内核也有它的好处,比如说微内核体积更小、可移植性更强。不过我认为,随着计算能力、存储技术越来越发达,体积小、安装快已经不能算是一个很大的优势了。现在更重要的是如何有效利用硬件设备的性能。
这一讲我们学习了内核的基础知识,包括内核的作用、整体架构以及 3 种内核类型(宏内核、微内核和混合类型内核)。内核很小(微内核)方便移植,因为体积小、安装快;内核大(宏内核),方便优化性能,毕竟内核更了解计算机中的资源。我们还学习了操作系统对执行文件的抽象,但是没有很深入讨论,内核部分有很多知识是需要在后面的几个模块中体现的,比如进程、文件、内存相关的能力等。
===
Linux 内核和 Windows 内核有什么区别?
【解析】 Windows 有两个内核,最新的是 NT 内核,目前主流的 Windows 产品都是 NT 内核。NT 内核和 Linux 内核非常相似,没有太大的结构化差异。
从整体设计上来看,Linux 是宏内核,NT 内核属于混合型内核。和微内核不同,宏内核和混合类型内核从实现上来看是一个完整的程序。只不过混合类型内核内部也抽象出了微内核的概念,从内核内部看混合型内核的架构更像微内核。
另外 NT 内核和 Linux 内核还存在着许多其他的差异,比如:
Linux 内核是一个开源的内核;
它们支持的可执行文件格式不同;
它们用到的虚拟化技术不同。
===用户态和内核态===
用户态线程和内核态线程有什么区别?
什么是用户态和内核态
Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高的权限。按照权限管理的原则,多数应用程序应该运行在最小权限下。因此,很多操作系统,将内存分成了两个区域:
内核空间(Kernal Space),这个空间只有内核程序可以访问;
用户空间(User Space),这部分内存专门给应用程序使用。
用户态和内核态
用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态(User Mode) 执行。内核空间中的代码可以访问所有内存,我们称这些程序在内核态(Kernal Mode) 执行。
系统调用过程
内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。当发生系统调用时,用户态的程序发起系统调用。因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是Trap(Trap是一种中断)。发生中断后,当前 CPU执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调用。内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
==进程和线程==
一个应用程序启动后会在内存中创建一个执行副本,这就是进程。Linux 的内核是一个 Monolithic Kernel(宏内核),因此可以看作一个进程。也就是开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。
进程可以分成用户态进程和内核态进程两类。用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。
那么用户态进程如果要执行程序,是否也要向内核申请呢?
程序在现代操作系统中并不是以进程为单位在执行,而是以一种轻量级进程(Light Weighted Process),也称作线程(Thread)的形式执行。
一个进程可以拥有多个线程。进程创建的时候,一般会有一个主线程随着进程创建而创建。
你可能会问,难道不是用户态的进程创建用户态的线程,内核态的进程创建内核态的线程吗?
其实不是,进程可以通过API创建用户态的线程,也可以通过系统调用创建内核态的线程,接下来我们说说用户态的线程和内核态的线程。
=用户态线程=
用户态线程也称作用户级线程(User LevelThread)。操作系统内核并不知道它的存在,它完全是在用户空间中创建。
用户级线程有很多优势,比如。
管理开销小:创建、销毁不需要系统调用。
切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。
但是这种线程也有很多的缺点。
与内核协作成本高:比如这种线程完全是用户空间程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换。
线程间协作成本高:设想两个线程需要通信,通信需要 I/O,I/O 需要系统调用,因此用户态线程需要支付额外的系统调用成本。
无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核的优势。
操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。
=内核态线程=
内核态线程也称作内核级线程(Kernel Level Thread)。这种线程执行在内核态,可以通过系统调用创造一个内核级线程。
内核级线程有很多优势。
可以利用多核 CPU 优势:内核拥有较高权限,因此可以在多个 CPU 核心上执行内核线程。
操作系统级优化:内核中的线程操作 I/O 不需要进行系统调用;一个内核线程阻塞了,可以立即让另一个执行。
当然内核线程也有一些缺点。
创建成本高:创建的时候需要系统调用,也就是切换到内核态。
扩展性差:由一个内核程序管理,不可能数量太多。
切换成本较高:切换的时候,也同样存在需要内核操作,需要切换内核态。