This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
<audio id="audio" title="60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/51/333d3d5a45fa87ff0761fd8ad0d28151.mp3"></audio>
操作系统的理论部分我们就讲完了,但是计算机这门学科是实验性的。为了更加深入地了解操作系统的本质,我们必须能够做一些上手实验。操作系统的实验,相比其他计算机课程的实验要更加复杂一些。
我们做任何实验都需要一个实验环境。这个实验环境要搭建在操作系统之上但是我们这个课程本身就是操作系统实验难不成要自己debug自己到底该咋整呢
我们有一个利器那就是qemu啊不知道你还记得吗它可以在操作系统之上模拟一个操作系统就像一个普通的进程。那我们是否可以像debug普通进程那样通过qemu来debug虚拟机里面的操作系统呢
这一节和下一节,我们就按照这个思路,来试试看,搭建一个操作系统的实验环境。
运行一个qemu虚拟机首先我们要有一个虚拟机的镜像。咱们在[虚拟机](https://time.geekbang.org/column/article/108964)那一节,已经制作了一个虚拟机的镜像。假设我们要基于 [ubuntu-18.04.2-live-server-amd64.iso](http://ubuntu-18.04.2-live-server-amd64.iso)它对应的内核版本是linux-source-4.15.0。
当时我们启动虚拟机的过程很复杂,设置参数的时候也很复杂,以至于解析这些参数就花了我们一章的时间。所以,这里我介绍一个简单的创建和管理虚拟机的方法。
在[CPU虚拟化](https://time.geekbang.org/column/article/109335)那一节我留过一个思考题OpenStack是如何创建和管理虚拟机的当时我给了你一个提示就是用libvirt。没错这一节我们就用libvirt来创建和管理虚拟机。
## 创建虚拟机
首先在宿主机上我们需要一个网桥。我们用下面的命令创建一个网桥并且设置一个IP地址。
```
brctl addbr br0
ip link set br0 up
ifconfig br0 192.168.57.1/24
```
为了访问外网,这里还需要设置/etc/sysctl.conf文件中net.ipv4.ip_forward=1参数并且执行以下的命令设置NAT。
```
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
```
接下来就要创建虚拟机了。这次我们就不再一个个指定虚拟机启动的参数而是用libvirt。首先使用下面的命令安装libvirt。
```
apt-get install libvirt-bin
apt-get install virtinst
```
libvirt管理qemu虚拟机是基于XML文件这样容易维护。
```
&lt;domain type='qemu'&gt;
&lt;name&gt;ubuntutest&lt;/name&gt;
&lt;uuid&gt;0f0806ab-531d-6134-5def-c5b4955292aa&lt;/uuid&gt;
&lt;memory unit='GiB'&gt;4&lt;/memory&gt;
&lt;currentMemory unit='GiB'&gt;4&lt;/currentMemory&gt;
&lt;vcpu placement='static'&gt;2&lt;/vcpu&gt;
&lt;os&gt;
&lt;type arch='x86_64' machine='pc-i440fx-trusty'&gt;hvm&lt;/type&gt;
&lt;boot dev='hd'/&gt;
&lt;/os&gt;
&lt;features&gt;
&lt;acpi/&gt;
&lt;apic/&gt;
&lt;pae/&gt;
&lt;/features&gt;
&lt;clock offset='utc'/&gt;
&lt;on_poweroff&gt;destroy&lt;/on_poweroff&gt;
&lt;on_reboot&gt;restart&lt;/on_reboot&gt;
&lt;on_crash&gt;restart&lt;/on_crash&gt;
&lt;devices&gt;
&lt;emulator&gt;/usr/bin/qemu-system-x86_64&lt;/emulator&gt;
&lt;disk type='file' device='disk'&gt;
&lt;driver name='qemu' type='qcow2'/&gt;
&lt;source file='/mnt/vdc/ubuntutest.img'/&gt;
&lt;target dev='vda' bus='virtio'/&gt;
&lt;/disk&gt;
&lt;controller type='pci' index='0' model='pci-root'/&gt;
&lt;interface type='bridge'&gt;
&lt;mac address='fa:16:3e:6e:89:ce'/&gt;
&lt;source bridge='br0'/&gt;
&lt;target dev='tap1'/&gt;
&lt;model type='virtio'/&gt;
&lt;/interface&gt;
&lt;serial type='pty'&gt;
&lt;target port='0'/&gt;
&lt;/serial&gt;
&lt;console type='pty'&gt;
&lt;target type='serial' port='0'/&gt;
&lt;/console&gt;
&lt;graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'&gt;
&lt;listen type='address' address='0.0.0.0'/&gt;
&lt;/graphics&gt;
&lt;video&gt;
&lt;model type='cirrus'/&gt;
&lt;/video&gt;
&lt;/devices&gt;
&lt;/domain&gt;
```
在这个XML文件中/mnt/vdc/ubuntutest.img就是虚拟机的镜像br0就是我们创建的网桥连接到网桥上的网卡libvirt会自动帮我们创建。
接下来需要将这个XML保存为domain.xml然后调用下面的命令交给libvirt进行管理。
```
virsh define domain.xml
```
接下来运行virsh list --all我们就可以看到这个定义好的虚拟机了然后我们调用virsh start ubuntutest启动这个虚拟机。
```
# virsh list
Id Name State
----------------------------------------------------
1 ubuntutest running
```
我们可以通过ps查看libvirt启动的qemu进程。这个命令行是不是很眼熟我们之前花了一章来讲解。如果不记得了你可以回去看看前面的内容。
```
# ps aux | grep qemu
libvirt+ 9343 85.1 34.7 10367352 5699400 ? Sl Jul27 1239:18 /usr/bin/qemu-system-x86_64 -name ubuntutest -S -machine pc-i440fx-trusty,accel=tcg,usb=off -m 4096 -realtime mlock=off -smp 2,sockets=2,cores=1,threads=1 -uuid 0f0806ab-531d-6134-5def-c5b4955292aa -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-ubuntutest/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 -drive file=/mnt/vdc/ubuntutest.img,format=qcow2,if=none,id=drive-virtio-disk0 -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 -netdev tap,fd=26,id=hostnet0 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:6e:89:ce,bus=pci.0,addr=0x3 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -vnc 0.0.0.0:0 -device cirrus-vga,id=video0,bus=pci.0,addr=0x2 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x5 -msg timestamp=on
```
从这里我们可以看到VNC的设置为0.0.0.0:0。我们可以用VNCViewer工具登录到这个虚拟机的界面但是这样实在是太麻烦了其实virsh有一个特别好的工具但是需要在虚拟机里面配置一些东西。
在虚拟机里面,我们修改/boot/grub/里面的两个文件一个是grub.cfg另一个是menu.lst这里面就是咱们在[系统初始化](https://time.geekbang.org/column/article/89739)的时候,讲过的那个启动列表。
在grub.cfg中在submenu Advanced options for Ubuntu 这一项在这一行的linux /boot/vmlinuz-4.15.0-55-generic root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity中加上了console=ttyS0。
```
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
menuentry 'Ubuntu, with Linux 4.15.0-55-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.0-55-generic-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_gpt
insmod ext2
set root='hd0,gpt2'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
else
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
fi
echo 'Loading Linux 4.15.0-55-generic ...'
linux /boot/vmlinuz-4.15.0-55-generic root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-4.15.0-55-generic
}
```
在menu.lst文件中在Ubuntu 18.04.2 LTS, kernel 4.15.0-55-generic这一项在kernel /boot/vmlinuz-4.15.0-55-generic root=/dev/hda1 ro console=hvc0 console=ttyS0这一行加入console=ttyS0。
```
title Ubuntu 18.04.2 LTS, kernel 4.15.0-55-generic
root (hd0)
kernel /boot/vmlinuz-4.15.0-55-generic root=/dev/hda1 ro console=hvc0 console=ttyS0
initrd /boot/initrd.img-4.15.0-55-generic
```
接下来我们重启虚拟机重启后上面的配置就起作用了。这时候我们可以通过下面的命令进入机器的控制台可以不依赖于SSH和IP地址进行登录。
```
# virsh console ubuntutest
Connected to domain ubuntutest
Escape character is ^]
```
下面我们可以配置这台机器的IP地址了。对于ubuntu-18.04来讲IP地址的配置方式为修改/etc/netplan/50-cloud-init.yaml文件。
```
network:
ethernets:
ens3:
addresses: [192.168.57.100/24]
gateway4: 192.168.57.1
dhcp4: no
nameservers:
addresses: [8.8.8.8,114.114.114.114]
optional: true
version: 2
```
然后我们可以通过netplan apply让配置生效这样虚拟机里面的IP地址就配置好了。现在我们应该能ping得通公网的一个网站了。
虚拟机就此创建好了,接下来我们需要下载源代码重新编译。
## 下载源代码
首先,我们先下载源代码。
```
apt-get install linux-source-4.15.0
```
这行命令会将代码下载到/usr/src/目录下,我们可以通过下面的命令解压缩。
```
tar vjxkf linux-source-4.15.0.tar.bz2
```
至此,路径/usr/src/linux-source-4.15.0下,就是解压好的内核代码。
准备工作都做好了。这一节,我们先来做第一个实验,也就是,在原有内核代码的基础上加一个我们自己的系统调用。
在哪里加代码呢?如果你忘了,请出门左转,回顾一下[系统调用](https://time.geekbang.org/column/article/90394)那一节。
第一个要加的地方是arch/x86/entry/syscalls/syscall_64.tbl。这里面登记了所有的系统调用号以及相应的处理函数。
```
332 common statx sys_statx
333 64 sayhelloworld sys_sayhelloworld
```
在这里我们找到332号系统调用sys_statx然后照猫画虎添加一个sys_sayhelloworld这里我们只添加64位操作系统的。
第二个要加的地方是include/linux/syscalls.h也就是系统调用的头文件然后添加一个系统调用的声明。
```
asmlinkage long sys_statx(int dfd, const char __user *path, unsigned flags,
unsigned mask, struct statx __user *buffer);
asmlinkage int sys_sayhelloworld(char * words, int count);
```
同样我们找到sys_statx的声明照猫画虎声明一个sys_sayhelloworld。其中words参数是用户态传递给内核态的文本的指针count是数目。
第三个就是对于这个系统调用的实现方便起见我们不再用SYSCALL_DEFINEx系列的宏来定义了直接在kernel/sys.c中实现。
```
asmlinkage int sys_sayhelloworld(char * words, int count){
int ret;
char buffer[512];
if(count &gt;= 512){
return -1;
}
copy_from_user(buffer, words, count);
ret=printk(&quot;User Mode says %s to the Kernel Mode!&quot;, buffer);
return ret;
}
```
接下来就要开始编译内核了。
## 编译内核
编译之前,我们需要安装一些编译要依赖的包。
```
apt-get install libncurses5-dev libssl-dev bison flex libelf-dev gcc make openssl libc6-dev
```
首先,我们要定义编译选项。
```
make menuconfig
```
然后我们能通过选中下面的选项激活CONFIG_DEBUG_INFO和CONFIG_FRAME_POINTER选项。
```
Kernel hacking ---&gt;
Compile-time checks and compiler options ---&gt;
[*] Compile the kernel with debug info
[*] Compile the kernel with frame pointers
```
选择完毕之后,配置会保存在.config文件中。如果我们打开看能看到这样的配置
```
CONFIG_FRAME_POINTER=y
CONFIG_DEBUG_INFO=y
```
接下来,我们编译内核。
```
nohup make -j8 &gt; make1.log 2&gt;&amp;1 &amp;
nohup make modules_install &gt; make2.log 2&gt;&amp;1 &amp;
nohup make install &gt; make3.log 2&gt;&amp;1 &amp;
```
这是一个非常长的过程请耐心等待可能需要数个小时因而这里用了nohup你可以去干别的事情。
当编译完毕之后grub和menu.lst都会发生改变。例如grub.conf里面会多一个新内核的项。
```
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
menuentry 'Ubuntu, with Linux 4.15.18' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.18-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_gpt
insmod ext2
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
else
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
fi
echo 'Loading Linux 4.15.18 ...'
linux /boot/vmlinuz-4.15.18 root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-4.15.18
}
```
例如menu.lst也多了新的内核的项。
```
title Ubuntu 18.04.2 LTS, kernel 4.15.18
root (hd0)
kernel /boot/vmlinuz-4.15.18 root=/dev/hda1 ro console=hvc0 console=ttyS0
initrd /boot/initrd.img-4.15.18
```
别忘了这里面都要加上console=ttyS0。
下面我们要做的就是重启虚拟机。进入的时候会出现GRUB界面。我们选择Ubuntu高级选项然后选择第一项进去通过uname命令我们就进入了新的内核。
```
# uname -a
Linux popsuper 4.15.18 #1 SMP Sat Jul 27 13:43:42 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
```
进入新的系统后,我们写一个测试程序。
```
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;linux/kernel.h&gt;
#include &lt;sys/syscall.h&gt;
#include &lt;string.h&gt;
int main ()
{
char * words = &quot;I am liuchao from user mode.&quot;;
int ret;
ret = syscall(333, words, strlen(words)+1);
printf(&quot;return %d from kernel mode.\n&quot;, ret);
return 0;
}
```
然后我们能利用gcc编译器编译后运行。如果我们查看日志/var/log/syslog就能够看到里面打印出来下面的日志这说明我们的系统调用已经添加成功了。
```
Aug 1 06:33:12 popsuper kernel: [ 2048.873393] User Mode says I am liuchao from user mode. to the Kernel Mode!
```
## 总结时刻
这一节是一节实战课我们创建了一台虚拟机在里面下载源代码尝试修改了Linux内核添加了一个自己的系统调用并且进行了编译并安装了新内核。如果你按照这个过程做下来你会惊喜地发现原来令我们敬畏的内核也是能够加以干预为我而用的呢。没错这就是你开始逐渐掌握内核的重要一步。
## 课堂练习
这一节的课堂练习,希望你能够按照整个过程,一步一步操作下来。毕竟看懂不算懂,做出来才算入门啊。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。

View File

@@ -0,0 +1,287 @@
<audio id="audio" title="61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/dd/f32e425858449891e10cb7ed546227dd.mp3"></audio>
上一节我们做了一个实验,添加了一个系统调用,并且编译了内核。这一节,我们来尝试调试内核。这样,我们就可以一步一步来看,内核的代码逻辑执行到哪一步了,对应的变量值是什么。
## 了解gdb
在Linux下面调试程序使用一个叫作gdb的工具。通过这个工具我们可以逐行运行程序。
例如上一节我们写的syscall.c这个程序我们就可以通过下面的命令编译。
```
gcc -g syscall.c
```
其中,参数-g的意思就是在编译好的二进制程序中加入debug所需的信息。
接下来我们安装一下gdb。
```
apt-get install gdb
```
然后,我们就可以来调试这个程序了。
```
~/syscall# gdb ./a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.1) 8.1.0.20180409-git
......
Reading symbols from ./a.out...done.
(gdb) l
1 #include &lt;stdio.h&gt;
2 #include &lt;stdlib.h&gt;
3 #include &lt;unistd.h&gt;
4 #include &lt;linux/kernel.h&gt;
5 #include &lt;sys/syscall.h&gt;
6 #include &lt;string.h&gt;
7
8 int main ()
9 {
10 char * words = &quot;I am liuchao from user mode.&quot;;
(gdb) b 10
Breakpoint 1 at 0x6e2: file syscall.c, line 10.
(gdb) r
Starting program: /root/syscall/a.out
Breakpoint 1, main () at syscall.c:10
10 char * words = &quot;I am liuchao from user mode.&quot;;
(gdb) n
12 ret = syscall(333, words, strlen(words)+1);
(gdb) p words
$1 = 0x5555555547c4 &quot;I am liuchao from user mode.&quot;
(gdb) s
__strlen_sse2 () at ../sysdeps/x86_64/multiarch/../strlen.S:79
(gdb) bt
#0 __strlen_sse2 () at ../sysdeps/x86_64/multiarch/../strlen.S:79
#1 0x00005555555546f9 in main () at syscall.c:12
(gdb) c
Continuing.
return 63 from kernel mode.
[Inferior 1 (process 1774) exited normally]
(gdb) q
```
在上面的例子中我们只要掌握简单的几个gdb的命令就可以了。
- l即list用于显示多行源代码。
- b即break用于设置断点。
- r即run用于开始运行程序。
- n即next用于执行下一条语句。如果该语句为函数调用则不会进入函数内部执行。
- p即print用于打印内部变量值。
- s即step用于执行下一条语句。如果该语句为函数调用则进入函数执行其中的第一条语句。
- c即continue用于继续程序的运行直到遇到下一个断点。
- bt即backtrace用于查看函数调用信息。
- q即quit用于退出gdb环境。
## Debug kernel
看了debug一个进程还是简单的接下来我们来试着debug整个kernel。
第一步要想kernel能够被debug需要像上面编译程序一样将debug所需信息也放入二进制文件里面去。这个我们在编译内核的时候已经设置过了也就是把“CONFIG_DEBUG_INFO”和“CONFIG_FRAME_POINTER”两个变量设置为yes。
第二步就是安装gdb。kernel运行在qemu虚拟机里面gdb运行在宿主机上所以我们应该在宿主机上进行安装。
第三步找到gdb要运行的那个内核的二进制文件。这个文件在哪里呢根据grub里面的配置它应该在/boot/vmlinuz-4.15.18这里。
另外为了方便在debug的过程中查看源代码我们可以将/usr/src/linux-source-4.15.0整个目录都拷贝到宿主机上来。因为内核一旦进入debug模式就不能运行了。
```
scp -r popsuper@192.168.57.100:/usr/src/linux-source-4.15.0 ./
```
在/usr/src/linux-source-4.15.0这个目录下面vmlinux文件也是内核的二进制文件。
第四步修改qemu的启动参数和qemu里面虚拟机的启动参数从而使得gdb可以远程attach到qemu里面的内核上。
我们知道gdb debug一个进程的时候gdb会监控进程的运行使得进程一行一行地执行二进制文件。如果像syscall.c的二进制文件a.out一样就在本地gdb可以通过attach到这个进程上作为这个进程的父进程来监控它的运行。
但是gdb debug一个内核的时候因为内核在qemu虚拟机里面所以我们无法监控本地进程而要通过qemu来监控qemu里面的内核这就要借助qemu的机制。
qemu有个参数-s它代表参数-gdb tcp::1234意思是qemu监听1234端口gdb可以attach到这个端口上来debug qemu里面的内核。
为了完成这一点我们需要修改ubuntutest这个虚拟机的定义文件。
```
virsh edit ubuntutest
```
在这里,我们能将虚拟机的定义文件修改成下面的样子,其中主要改了两项:
- 在domain的最后加上了qemu:commandline里面指定了参数-s
- 在domain中添加xmlns:qemu。没有这个XML的namespaceqemu:commandline这个参数libvirt不认。
```
&lt;domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'&gt;
&lt;name&gt;ubuntutest&lt;/name&gt;
&lt;uuid&gt;0f0806ab-531d-6134-5def-c5b4955292aa&lt;/uuid&gt;
&lt;memory unit='KiB'&gt;8388608&lt;/memory&gt;
&lt;currentMemory unit='KiB'&gt;8388608&lt;/currentMemory&gt;
&lt;vcpu placement='static'&gt;8&lt;/vcpu&gt;
&lt;os&gt;
&lt;type arch='x86_64' machine='pc-i440fx-trusty'&gt;hvm&lt;/type&gt;
&lt;boot dev='hd'/&gt;
&lt;/os&gt;
&lt;clock offset='utc'/&gt;
&lt;on_poweroff&gt;destroy&lt;/on_poweroff&gt;
&lt;on_reboot&gt;restart&lt;/on_reboot&gt;
&lt;on_crash&gt;restart&lt;/on_crash&gt;
&lt;devices&gt;
&lt;emulator&gt;/usr/bin/qemu-system-x86_64&lt;/emulator&gt;
&lt;disk type='file' device='disk'&gt;
&lt;driver name='qemu' type='qcow2'/&gt;
&lt;source file='/mnt/vdc/ubuntutest.img'/&gt;
&lt;backingStore/&gt;
&lt;target dev='vda' bus='virtio'/&gt;
&lt;alias name='virtio-disk0'/&gt;
&lt;address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/&gt;
&lt;/disk&gt;
......
&lt;interface type='bridge'&gt;
&lt;mac address='fa:16:3e:6e:89:ce'/&gt;
&lt;source bridge='br0'/&gt;
&lt;target dev='tap1'/&gt;
&lt;model type='virtio'/&gt;
&lt;alias name='net0'/&gt;
&lt;address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/&gt;
&lt;/interface&gt;
......
&lt;/devices&gt;
&lt;qemu:commandline&gt;
&lt;qemu:arg value='-s'/&gt;
&lt;/qemu:commandline&gt;
&lt;/domain&gt;
```
另外为了远程debug成功我们还需要修改qemu里面的虚拟机的grub和menu.list在内核命令行中添加nokaslr来关闭KASLR。KASLR会使得内核地址空间布局随机化从而会造成我们打的断点不起作用。
对于grub.conf修改如下
```
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
menuentry 'Ubuntu, with Linux 4.15.18' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.18-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_gpt
insmod ext2
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
else
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
fi
echo 'Loading Linux 4.15.18 ...'
linux /boot/vmlinuz-4.15.18 root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro nokaslr console=ttyS0 maybe-ubiquity
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-4.15.18
}
```
对于menu.list修改如下
```
title Ubuntu 18.04.2 LTS, kernel 4.15.18
root (hd0)
kernel /boot/vmlinuz-4.15.18 root=/dev/hda1 ro nokaslr console=hvc0 console=ttyS0
initrd /boot/initrd.img-4.15.18
```
修改完毕后我们需要在虚拟机里面shutdown -h now来关闭虚拟机。注意不要reboot因为虚拟机里面运行reboot我们改过的那个XML会不起作用。
当我们在宿主机上发现虚拟机关机之后就可以通过virsh start ubuntutest启动虚拟机这个时候我们添加的参数-s才起作用。
第五步使用gdb运行内核的二进制文件执行gdb vmlinux。
```
/mnt/vdc/linux-source-4.15.0# gdb vmlinux
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
......
To enable execution of this file add
add-auto-load-safe-path /mnt/vdc/linux-source-4.15.0/vmlinux-gdb.py
......
(gdb) b sys_sayhelloworld
Breakpoint 1 at 0xffffffff8109e2f0: file kernel/sys.c, line 192.
(gdb) target remote :1234
Remote debugging using :1234
native_safe_halt () at ./arch/x86/include/asm/irqflags.h:61
61 }
(gdb) c
Continuing.
[Switching to Thread 2]
Thread 2 hit Breakpoint 1, sys_sayhelloworld (words=0x563cbfa907c4 &quot;I am liuchao from user mode.&quot;, count=29) at kernel/sys.c:192
192 {
(gdb) bt
#0 sys_sayhelloworld (words=0x55b2811537c4 &quot;I am liuchao from user mode.&quot;, count=29) at kernel/sys.c:192
#1 0xffffffff810039f7 in do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:290
#2 0xffffffff81a00081 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:237
(gdb) n
195 if(count &gt;= 1024){
(gdb) n
198 copy_from_user(buffer, words, count);
(gdb) n
199 ret=printk(&quot;User Mode says %s to the Kernel Mode!&quot;, buffer);
(gdb) p buffer
$1 = &quot;I am liuchao from user mode.\000\177\000\000\...
(gdb) n
200 return ret;
(gdb) p ret
$2 = 63
(gdb) c
(gdb) n
do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:295
295 syscall_return_slowpath(regs);
(gdb) s
syscall_return_slowpath (regs=&lt;optimized out&gt;) at arch/x86/entry/common.c:295
(gdb) n
268 prepare_exit_to_usermode(regs);
(gdb) n
do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:296
296 }
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:246
246 movq RCX(%rsp), %rcx
......
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:330
330 USERGS_SYSRET64
```
我们先设置一个断点在我们自己写的系统调用上b sys_sayhelloworld通过执行target remote :1234来attach到qemu上然后执行c也即continue运行内核。这个时候内核始终在Continuing的状态也即持续在运行中这个时候我们可以远程登录到qemu里的虚拟机上执行各种命令。
如果我们在虚拟机里面运行syscall.c编译好的a.out这个时候肯定会调用到内核。内核肯定会经过系统调用的过程到达sys_sayhelloworld这个函数这就碰到了我们设置的那个断点。
如果执行bt我们能看到这个系统调用是从entry_64.S里面的entry_SYSCALL_64 ()函数调用到do_syscall_64函数再调用到sys_sayhelloworld函数的。这一点和我们在[系统调用](https://time.geekbang.org/column/article/90394)那一节分析的过程是一模一样的。
我们可以通过执行next命令来看sys_sayhelloworld一步一步是怎么执行的通过p buffer查看buffer里面的内容。在这个过程中由于内核是逐行运行的因而我们在虚拟机里面的命令行是卡死的状态。
当我们不断地next直到执行完毕sys_sayhelloworld的时候会看到do_syscall_64会调用syscall_return_slowpath。它会调用prepare_exit_to_usermode然后会回到entry_SYSCALL_64然后对于寄存器进行操作最后调用指令USERGS_SYSRET64回到用户态。这个返回的过程和系统调用那一节也一模一样。
通过debug我们能够跟踪系统调用的整个过程。你可以将我们这一门课里面学的所有的过程都debug一下看看变量的值从而对于内核的工作机制有更加深入的了解。
## 总结时刻
在这个课程里面我们写过一些程序为了保证程序能够顺利运行我一般会将代码完整地放到文本中让你拷贝下来就能编译和运行。如果你运行的时候发现有问题或者想了解一步一步运行的细节这一节介绍的gdb是一个很好的工具。
这一节你尤其应该掌握的是如何通过宿主机上的gdb来debug虚拟机里面的内核。这一点非常重要会了这个你就能够返回去挨个研究每一章每一节的内核数据结构和运行逻辑了。
在这门课中进程管理、内存管理、文件管理、设备管理网络管理我们都介绍了从系统调用到底层的整个逻辑。如果你对我前面的代码解析还比较困惑你可以尝试着去debug这些过程只要把断点打在系统调用的入口位置就可以了。
从此开启你的内核debug之旅吧
## 课堂练习
这里给你留一道题目你可以试着debug一下文件打开的过程。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="62 | 知识串讲:用一个创业故事串起操作系统原理(一)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/31/1a/31ccc375346c1a58f049aeaefbb4781a.mp3"></audio>
操作系统是一门体系复杂、知识点很多的课程,经过这么多节的讲解,你是否已经感觉自己被淹没在细节的汪洋大海里面了?没关系,从这一节开始,我们用五节的时间,通过一个创业故事,串起来操作系统的整个知识体系。
接下来,我们就来看主人公是如何从小马,变成马哥,再变成马总的吧!
## 小马创业选园区,开放标准是第一
小马最终还是决定走出大公司,自己去创业了。
他之所以这样决定有两个原因一方面大企业多年的工作经验让他练就了从前端到后端从Web到App从产品设计到测试交付的全栈能力。他很自信靠着这些能力闯荡江湖应该没什么问题另外一方面他听说姓“马”的创业成功的概率好像比较大。
创业首先要注册公司。注册公司就需要有一个办公地点。所以小马需要选择一个适合创业的环境。他找了很多地方发现有的地方政策倾斜大型企业有的地方倾斜本地企业有的地方鼓励金融创新。小马感觉这些地方都不太适合他这个IT男。
直到有一天小马来到了位于杭州滨江的x86创业园区。他被深深地吸引住了当然首要吸引他的就是园区工作人员的热情。
园区的工作人员向小马介绍了以下信息。
“首先咱们这个x86园区主要有三大特点一是标准二是开放三是兼容。像您这种创业者还是非常多的。初次创业不一定有经验园区提供标准的企业运行流程辅导。”
“另外,我们园区秉承完全开放的态度,对待各种各样的企业。不封闭,不保守。只要您符合国家的法律法规,我们都接纳。而且,整个园区是一种开放合作的生态,也有利于不同企业之间的协作。”
“再就是兼容。我们园区的流程和规则的设计都会兼容历史上的既有政策,既不会朝令夕改,也不会因为变化而影响您公司的运转。总而言之,来了咱们园区,您就埋头干业务就可以啦!”
小马显然对于x86园区的开放性十分满意于是追问道“您刚才说的企业运行流程辅导能详细介绍一下吗将来我这个企业在这个园区应该怎么个运转法儿
工作人员接着说“咱们这个园区毗邻全国知名高校每年都有大量的优秀毕业生来园区找工作这是企业非常重要的人才来源。葛优说了二十一世纪了人才是核心嘛。每年我们园区都会招聘大量的毕业生先进行一个月的培训合格毕业的可以推荐给您这种企业。这些人才啊就是咱们企业的CPU。”
“经过我们园区培训过的CPU人才具备了三种老板们喜欢的核心竞争力
第一,实干能力强,干活快,我们称为运算才能——也即指令执行能力;
第二,记忆力好,记得又快又准,我们称为数据才能——也即数据寄存能力;
第三,听话,自控能力强,可以多任务并发执行,我们称为控制才能——也即指令寄存能力。
到时候你可以根据需求看雇用多少个CPU人才
另外,人才得有个办公的地方,这一片呢,就是我们的办公区域,称为也就是内存区域。您可以包几个工位,或者包一片区域,或者几个会议室,让您公司的人才在里面做项目就可以了。这里面有的是地方,同时运行多少个项目都行。”
<img src="https://static001.geekbang.org/resource/image/3a/23/3afda18fc38e7e53604e9ebf9cb42023.jpeg" alt="">
跟着工作人员的介绍小马走在x86园区中看着这一片片的内存办公区脑子里已经浮现出将来热火朝天的办公场景了。
“也许不到半年的时间我肯定能够接两三个大项目招聘十个八个CPU员工。那项目A的员工就坐在这片内存办公区项目B的员工就坐在那片内存办公区。我根据积累的人脉将接到的项目写成一个一个的项目执行计划书里面是一行行项目执行的指令这些指令操作数据产生一些结果我们就可以叫程序啦。”小马这么想着。
“然后呢我把不同的项目执行计划书交给不同的项目组去执行。那项目组就叫进程吧两个项目组进程A和B会有独立的内存办公空间互相隔离程序会分别加载到进程A和进程B的内存办公空间里面形成各自的代码段。要操作的数据和产生的结果就放在数据段里面。“
“除此之外我应该找一个或者多个CPU员工来运行项目执行计划书我只要告诉他下一条指令在内存办公区中的地址经过训练的CPU员工就会很自觉地、不停地将代码段的指令拿进来进行处理。“
“指令一般是分两部分一部分表示做什么操作例如是加法还是位移另一部分是操作哪些数据。数据的部分CPU员工会从数据段里面读取出来记在脑子里然后进行处理处理完毕的结果再写回数据段。当项目执行计划书里面的所有指令都执行完毕之后项目也就完成了那就可以等着收钱啦。”
小马沉浸在思绪中久久不能自拔,直到工作人员打断了他的思绪:“您觉得园区如何?要不要入住呀?先租几个工位,招聘几个人呢?“
小马想了想,说道:“园区我很满意,以后就在您这里创业了,创业开始,我先不招人,自己先干吧。”
## 启动公司有手册,获取内核当宝典
工作人员说“感谢您入驻咱们创业园区由于您是初次创业这里有一本《创业指导手册》在这一本叫作BIOS的小册子上有您启动一家公司的通用流程你只要按照里面做就可以了。”
小马接过BIOS小册子开始按照里面的指令启动公司了。
创业初期小马的办公室肯定很小只有有1M的内存办公空间。在1M空间最上面的0xF0000到0xFFFFF这64K映射给ROM通过读这部分地址可以访问这个BIOS小册子里面的指令。
创业指导手册第一条BIOS要检查一些系统的硬件是不是都好着呢。创业指导手册第二条要有个办事大厅只不过小马自己就是办事员。因为一旦开张营业就会有人来找到这家公司因而基本的中断向量表和中断服务程序还是需要的至少要能够使用键盘和鼠标。
BIOS这个手册空间有限只能帮小马把公司建立起来公司如何运转和经营就需要另外一个东西——《企业经营宝典》因而BIOS还要做的一件事情就是帮助小马找到这个宝典然后让小马以后根据这个宝典里面的方法来经营公司这个《企业经营宝典》就是这家公司的内核。
<img src="https://static001.geekbang.org/resource/image/0a/6b/0a29c1d3e1a53b2523d2dcab3a59886b.jpeg" alt="">
运营一个企业非常的复杂因而这本《企业经营宝典》也很厚BIOS手册无法直接加载出来而需要从门卫开始问起不断打听这本内核的位置然后才能加载他。
门卫只有巴掌大的一块地方在启动盘的第一个扇区512K的大小我们通常称为MBRMaster Boot Record主引导记录/扇区。这里保存了boot.imgBIOS手册会将他加载到内存中的0x7c00来运行。
boot.img做不了太多的事情。他能做的最重要的一个事情就是加载grub2的另一个镜像core.img。
引导扇区就是小马找到的门卫,虽然他看着档案库的大门,但是知道的事情很少。他不知道宝典在哪里,但是,他知道应该问谁。门卫说,档案库入口处有个管理处,然后把小马领到门口。
core.img就是管理处他们知道的和能做的事情就多了一些。core.img由lzma_decompress.img、diskboot.img、kernel.img和一系列的模块组成功能比较丰富能做很多事情。
boot.img将控制权交给diskboot.img后diskboot.img的任务就是将core.img的其他部分加载进来先是解压缩程序lzma_decompress.img再往下是kernel.img最后是各个模块module对应的映像。
管理处听说小马要找宝典,知道他将来是要做老板的人。管理处就告诉小马,既然是老板,早晚都要雇人干活的。这不是个体户小打小闹,所以,你需要切换到老板角色,进入保护模式,把哪些是你的权限,哪些是你可以授权给别人的,都分得清清楚楚。
这些,小马都铭记在心,此时此刻,虽然公司还是只有他一个人,但是小马的眼界放宽了,能够管理的内存空间大多了,也开始区分哪些是用户态,哪些是内核态了。
接下来kernel.img里面的grub_main会给小马展示一个《企业经营宝典》的列表也即操作系统的列表让小马进行选择。经营企业的方式也有很多种到底是人性化的还是强纪律的这个时候你要做一个选择。
<img src="https://static001.geekbang.org/resource/image/f1/b6/f1be2db375f1503af85535dec5efe9b6.png" alt="">
在这里小马毫不犹豫地选择了《狼性文化》操作系统至此grub才开始启动《狼性文化》操作系统内核。
拿到了宝典的小马,开始越来越像一个老板了。他要开始以老板的思维,来建立这家公司。
## 初创公司有章法,请来兄弟做臂膀
这注定是一个不眠夜,办公室里面一片漆黑中,唯一亮着的台灯下,小马独自捧着《企业经营宝典》仔细研读,读着读着,小马若有所思,开始书写公司内核的初始化计划。
<img src="https://static001.geekbang.org/resource/image/75/cd/758c283cf7633465d24ab3ef778328cd.jpeg" alt="">
公司首先应该有个项目管理部门咱们将来肯定要接各种各样的项目因此项目管理体系和项目管理流程首先要建立起来。虽然现在还没有项目但是小马还是弄了一个项目模板init_task。这是公司的第一个项目进程是项目管理系统里面的项目列表中的第一个我们能称为0号进程。这个项目是虚拟的不对应一个真实的项目也就是进程
项目需要项目管理进行调度,还需要制定一些调度策略。
另外为了快速响应客户需求为了各个项目组能够方便地使用公司的公共资源还应该有一个办事大厅。这里面可以设置了很多中断门Interrupt Gate用于处理各种中断以便快速响应突发事件还可以提供系统调用为项目组服务。
如果项目接得多了,为了提高研发效率,对项目内容进行保密,就需要封闭开发,所以将来会有很多的会议室,因而还需要一个会议室管理系统。
项目的执行肯定会留下很多文档,这些是公司的积累,将来的核心竞争力,一定要好好管理,因而应该建立一个项目档案库,也即文件系统。
随着思绪的展开,小马奋笔疾书,已经写了满满的几页纸,小马顿感经营一个公司还是挺复杂的,一旦项目接多了肯定忙不过来。俗话说得好,“一个好汉三个帮”,小马准备找两个兄弟来一起创业。
小马想到的第一个人,是自己的大学室友,外号“周瑜”。大学一毕业,周瑜就转项目管理了,在一家大公司管理着大型项目。将来外部接了项目,可以让他来管。小马想到的第二个人,是自己上一家公司的同事,外号“张昭”,是他们总经理的好帮手,公司的流程、人事、财务打理得都清清楚楚,将来公司内部要运行得井井有条,也需要这样一个人。
第二天,小马请周瑜和张昭吃饭,邀请他们加入他的创业公司。小马说,公司要正规运转起来,应该分清内外,外部项目需要有人帮忙管理好——也就是用户态,内部公司的核心资源也需要管理好——也就是内核态。现在我一个人忙不过来,需要两位兄弟的加入,周瑜主外,张昭主内,正所谓,内事不决问张昭,外事不决问周郎嘛。
三个人相谈甚欢,谈及往日友谊、未来前景、上市敲钟……
第三天周瑜早早就来到公司开始了他的事业。小马拜托周瑜做的第一件事情是调用kernel_init运行1号项目进程。这个1号项目会在用户态运行init项目进程。这是第一个以外部项目的名义运行的之所以叫init就是做初始化的工作周瑜根据自己多年的项目管理经验将这个init项目立为标杆以后所有外部项目的运行都要按照他来是外部项目的祖先项目。
下午张昭也来到了公司小马拜托张昭做的第一件事情是调用kthreadd运行2号项目进程。这个2号项目是内核项目的祖先。将来所有的项目都有父项目、祖先项目会形成一棵项目树。公司大了之后周瑜和张昭做的公司VP级别的任务就可以坐在塔尖上了。
<img src="https://static001.geekbang.org/resource/image/4d/16/4de740c10670a92bbaa58348e66b7b16.jpeg" alt="">
好了,这一节小马终于将公司的架子搭起来了,兄弟三人如当年桃园三结义一样,开始自己的创业生涯,小马的这家公司能不能顺利接到项目呢?欲知后事,且听下回分解。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,136 @@
<audio id="audio" title="63 | 知识串讲:用一个创业故事串起操作系统原理(二)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/c2/de9daa81b093c3a723789fd2757dedc2.mp3"></audio>
上一节说到小马同学的公司已经创立了,还请来了周瑜和张昭作为帮手,所谓“兄弟齐心,其利断金”。可是,现在这家公司,还得从接第一个外部项目开始。
## 首个项目虽简单,项目管理成体系
<img src="https://static001.geekbang.org/resource/image/db/a9/dbd8785da6c3ce3fe1abb7bb5934b7a9.jpeg" alt="">
这第一个项目还是小马亲自去谈的。其实软件公司了解客户需求还是比较难的因为客户都说着接近人类的语言例如C/C++。这些咱们公司招聘的CPU小伙伴们可听不懂需要有一个人将客户需求转换为项目执行计划书CPU小伙伴们才能执行这个过程我们称为编译。
编译其实是一个需求分析和需求转换的过程。这个过程会将接近人类的C/C++语言转换为CPU小伙伴能够听懂的二进制语言并且以一定的文档格式写成项目执行计划书。这种文档格式是作为一个标准化的公司事先制定好的一种格式是周瑜从大公司里面借鉴来的称为ELF格式这个项目执行计划书有总论ELF Header的部分有包含指令的代码段的部分有包含全局变量的数据段的部分。
小马和客户聊了整整一天确认了项目的每一个细节保证编译能够通过才写成项目执行计划书ELF文件放到档案库中。此时已经半夜了。
第二天,周瑜一到公司,小马就兴奋地给周瑜说,“我昨天接到了第一个项目,而且是一个大项目,项目执行计划书我都写好了,你帮我监督、执行、管理,记得按时交付哦!”
周瑜说“没问题。”于是周瑜从父项目开始fork一个子项目然后在子项目中调用exec系统调用 然后到了内核里面通过load_elf_binary将项目执行计划书加载到子进程内存中交给一个CPU执行。
虽然这是第一个项目,以周瑜的项目管理经验,他告诉小马,项目的执行要保质保量,需要有一套项目管理系统来管理项目的状态,而不能靠脑子记。“项目管理系统?当然应该有了”,小马说。他在《企业经营宝典》中看到过。
于是项目管理系统就搭建起来了。在这里面所有项目都放在一个task_struct列表中对于每一个项目都非常详细地登记了项目方方面面的信息。
<img src="https://static001.geekbang.org/resource/image/1c/bc/1c91956b52574b62a4418a7c6993d8bc.jpeg" alt="">
每一个项目都应该有一个ID作为这个项目的唯一标识。到时候排期啊、下发任务啊等等都按ID来就不会产生歧义。
项目应该有运行中的状态TASK_RUNNING并不是说进程正在运行而是表示进程在时刻准备运行的状态。这个时候要看CPU小伙伴有没有空有空就运行他没空就得等着。
有时候进程运行到一半需要等待某个条件才能运行下去这个时候只能睡眠。睡眠状态有两种。一种是TASK_INTERRUPTIBLE可中断的睡眠状态。这是一种浅睡眠的状态也就是说虽然在睡眠等条件成熟进程可以被唤醒。
另一种睡眠是TASK_UNINTERRUPTIBLE不可中断的睡眠状态。这是一种深度睡眠状态不可被唤醒只能死等条件满足。有了一种新的进程睡眠状态TASK_KILLABLE可以终止的新睡眠状态。进程处于这种状态中他的运行原理类似TASK_UNINTERRUPTIBLE只不过可以响应致命信号也即虽然在深度睡眠但是可以被干掉。
一旦一个进程要结束先进入的是EXIT_ZOMBIE状态但是这个时候他的父进程还没有使用wait()等系统调用来获知他的终止信息,此时进程就成了僵尸进程。
EXIT_DEAD是进程的最终状态。
<img src="https://static001.geekbang.org/resource/image/e2/88/e2fa348c67ce41ef730048ff9ca4c988.jpeg" alt="">
另外,项目运行的统计信息也非常重要。例如,有的员工很长时间都在做一个任务,这个时候你就需要特别关注一下;再如,有的员工的琐碎任务太多,这会大大影响他的工作效率。
那如何才能知道这些员工的工作情况呢?在进程的运行过程中,会有一些统计量,例如进程在用户态和内核态消耗的时间、上下文切换的次数等等。
项目之间的亲缘关系也需要维护,任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。
<img src="https://static001.geekbang.org/resource/image/92/04/92711107d8dcdf2c19e8fe4ee3965304.jpeg" alt="">
另外,对于项目来讲,项目组权限的控制也很重要。什么是项目组权限控制呢?这么说吧,我这个项目组能否访问某个文件,能否访问其他的项目组,以及我这个项目组能否被其他项目组访问等等
另外,项目运行过程中占用的公司的资源,例如会议室(内存)、档案库(文件系统)也需要在项目管理系统里面登记。
周瑜同学将项目登记好然后就分配给CPU同学们说开始执行吧。
好在第一个项目还是比较简单的一个CPU同学按照项目执行计划书按部就班一条条地执行很快就完成了客户评价还不错很快收到了回款。
## 项目大了要并行,项目多了要排期
小马很开心可谓开门红。接着第二个项目就到来了这可是一个大项目要帮一家知名公司开发一个交易网站共200个页面这下要赚翻了就是时间要得比较急要求两个星期搞定。
小马把项目带回来周瑜同学说这个项目有点大估计一个CPU同学干不过来了估计要多个CPU同学一起协作了。
为了完成这个大的项目进程就不能一个人从头干到尾了这样肯定赶不上工期。于是周瑜将一个大项目拆分成20个子项目每个子项目完成10个页面一个大项目组也分成20个小组并行开发都开发完了再做一次整合这肯定比依次开发200个页面快多了。如果项目叫进程那子项目就叫线程。
在Linux里面无论是进程还是线程到了内核里面我们统一都叫任务由一个统一的结构task_struct进行管理。
<img src="https://static001.geekbang.org/resource/image/75/2d/75c4d28a9d2daa4acc1107832be84e2d.jpeg" alt="">
不知道是好消息,还是坏消息,这么大一个项目还没有做完,新的项目又找上门了。看来有了前面的标杆客户,名声算是打出去了,一个项目接一个地不停。
小马是既高兴又犯愁于是找周瑜和张昭商量应该咋办。要不多招人多来几个CPU小伙伴就不搞定了可是咱们还是在创业阶段养不起这么多人。另外的办法就是人力复用一个CPU小伙伴干多个项目干不过来就加加班实在不行就996这样应该就没问题了。
一旦涉及一个CPU小伙伴同时参与多个项目就非常考验项目管理的水平了。如何排期、如何调度是一个大学问。例如有的项目比较紧急应该先进行排期有的项目可以缓缓但是也不能让客户等太久。所以这个过程非常复杂需要平衡。
对于操作系统来讲他面对的CPU的数量是有限的干活儿都是他们但是进程数目远远超过CPU的数目因而就需要进行进程的调度有效地分配CPU的时间既要保证进程的最快响应也要保证进程之间的公平。
如何调度呢周瑜能够想到的方式就是排队。每一个CPU小伙伴旁边都有一个白板上面写着自己需要完成的任务来了新任务就写到白板上做完了就擦掉。
一个CPU上有一个队列队列里面是一系列sched_entity每个sched_entity都属于一个task_struct代表进程或者线程。
调度要解决的第一个问题是每一个CPU小伙伴每过一段时间都要想一下白板上这么多项目我应该干哪一个CPU的队列里面有这么多的进程或者线程应该取出哪一个来执行
<img src="https://static001.geekbang.org/resource/image/10/af/10381dbafe0f78d80beb87560a9506af.jpeg" alt="">
这就是调度规则或者调度算法的问题。
周瑜说,他原来在大公司的时候,调度算法常常是这样设计的。
一个是公平性对于接到的多个项目不能厚此薄彼。这个算法主要由fair_sched_class实现fair就是公平的意思。
另一个是优先级,有的项目要急一点,客户出的钱多,所以应该多分配一些精力在高优先级的项目里面。
在Linux里面讲究的公平可不是一般的公平而是CFS调度算法CFS全称是Completely Fair Scheduling完全公平调度。
为了公平项目经理需要记录下进程的运行时间。CPU会提供一个时钟过一段时间就触发一个时钟中断。就像咱们的表滴答一下这个我们叫Tick。CFS会为每一个进程安排一个虚拟运行时间vruntime。如果一个进程在运行随着时间的增长也就是一个个Tick的到来进程的vruntime将不断增大。没有得到执行的进程vruntime不变。
显然那些vruntime少的原来受到了不公平的对待需要给他补上所以会优先运行这样的进程。
这有点儿像让你把一筐球平均分到N个口袋里面你看着哪个少就多放一些哪个多了就先不放。这样经过多轮虽然不能保证球完全一样多但是也差不多公平。
有时候,进程会分优先级,如何给优先级高的进程多分时间呢?
这个简单就相当于N个口袋优先级高的袋子大优先级低的袋子小。这样球就不能按照个数分配了要按照比例来大口袋的放了一半和小口袋放了一半里面的球数目虽然差很多也认为是公平的。
函数update_curr用于更新进程运行的统计量vruntime CFS还需要一个数据结构来对vruntime进行排序找出最小的那个。在这里使用的是红黑树。红黑树的节点是sched_entity里面包含vruntime。
调度算法的本质就是解决下一个进程应该轮到谁运行的问题这个逻辑在fair_sched_class.pick_next_task中完成。
调度要解决的第二个问题是什么时候切换任务也即什么时候CPU小伙伴应该停下一个进程换另一个进程运行
一个人在做A项目在某个时刻换成做B项目去了。发生这种情况主要有两种方式。
方式一A项目做着做着里面有一条指令sleep也就是要休息一下或者等待某个I/O事件。那没办法了要主动让出CPU然后可以开始做B项目。主动让出CPU的进程会主动调用schedule()函数。
在schedule()函数中会通过fair_sched_class.pick_next_task在红黑树形成的队列上取出下一个进程然后调用context_switch进行进程上下文切换。
进程上下文切换主要干两件事情一是切换进程空间也即进程的内存也即CPU小伙伴不能A项目的会议室里面干活了要跑到B项目的会议室去。二是切换寄存器和CPU上下文也即CPU将当期在A项目中干到哪里了记录下来方便以后接着干。
方式二A项目做着做着旷日持久实在受不了了。项目经理介入了说这个项目A先停停B项目也要做一下要不然B项目该投诉了。最常见的现象就是A进程执行时间太长了是时候切换到B进程了。这个时候叫作A进程被被动抢占。
抢占还要通过CPU的时钟Tick来衡量进程的运行时间。时钟Tick一下是很好查看是否需要抢占的时间点。 时钟中断处理函数会调用scheduler_tick()他会调用fair_sched_class的task_tick_fair在这里面会调用update_curr更新运行时间。当发现当前进程应该被抢占不能直接把他踢下来而是把他标记为应该被抢占打上一个标签TIF_NEED_RESCHED。
另外一个可能抢占的场景发生在当一个进程被唤醒的时候。一个进程在等待一个I/O的时候会主动放弃CPU。但是当I/O到来的时候进程往往会被唤醒。这个时候是一个时机。当被唤醒的进程优先级高于CPU上的当前进程就会触发抢占。如果应该发生抢占也不是直接踢走当然进程而也是将当前进程标记为应该被抢占打上一个标签TIF_NEED_RESCHED。
真正的抢占还是需要上下文切换也就是需要那么一个时刻让正在运行中的进程有机会调用一下schedule。调用schedule有以下四个时机。
- 对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。
- 对于用户态的进程来讲,从中断中返回的那个时刻,也是一个被抢占的时机。
- 对内核态的执行中被抢占的时机一般发生在preempt_enable()中。在内核态的执行中有的操作是不能被中断的所以在进行这些操作之前总是先调用preempt_disable()关闭抢占。再次打开的时候,就是一次内核态代码被抢占的机会。
- 在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态。这个时候也是一个执行抢占的时机。
周瑜和张昭商定了这个规则然后给CPU小伙伴们交代之后项目虽然越来越多但是也井井有条起来。CPU小伙伴不会像原来一样火急火燎不知所从了。
可是其实对于项目的开发,这家公司还是有严重漏洞的,就是项目的保密问题,不管哪家客户将系统外包出去,肯定也不想让其他公司知道详情。如果解决不好这个问题,没人敢把重要的项目交给这家公司,小马的公司也就永远只能接点边角系统,还是不能保证温饱问题。
那接下来,小马会怎么解决项目之间的保密问题呢?欲知后事,且听下回分解。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,149 @@
<audio id="audio" title="64 | 知识串讲:用一个创业故事串起操作系统原理(三)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/07/19cc71d1d2659f99de7309a143b8bc07.mp3"></audio>
上一节我们说到周瑜和张昭商定了调用schedule的时机。尽管项目越来越多但是也井井有条。可是我们也说了不管你的事情做得有多好项目保密问题都是要解决的重要问题。怎么解决呢今天我们就来看一看。
## 保密需封闭开发,空间小巧妙安排
慢慢地小马发现项目接的多了之后CPU小伙伴的任务调度问题解决了之后会议室的使用经常陷入混乱。不同的项目使用会议室的时候经常冲突一个项目组没用完另一个项目组就在那里等着十分耽误开发效率。
小马说:“要不咱们的项目别用会议室封闭开发了,原来总是说封闭开发,就是为了隔离,保密。这对于公司声誉来说很重要,但是能不能通过签订保密协议的方式来,干嘛非得封闭开发呢?”
周瑜说:“马哥,以我在大公司管理项目的经验来看,您还是想简单了。”
“你看每次你接一个项目总要写成项目执行计划书CPU小伙伴们才能执行吧项目计划书中的一行一行指令运行过程中免不了要产生一些数据。这些数据要保存在一个地方这个地方就是会议室内存。会议室内存被分成一块一块儿的都编好了号。例如3F-10就是三楼十号会议室。这个地址是实实在在的地址通过这个地址我们就能够定位到物理内存的位置。”
“现在问题来了,写项目执行计划书的时候,里面的指令使用的地址是否可以使用物理地址呢?当然不行了,项目执行计划书,都是事先写好的,可以多次运行的。如果里面有个指令是,要把用户输入的数字保存在内存中,那就会有问题。”
“会产生什么问题呢我举个例子你就明白了。如果我们使用那个实实在在的地址3F-10打开三个相同的程序都执行到某一步。比方说打开了三个计算器用户在这三个程序的界面上分别输入了10、100、1000。如果内存中的这个位置只能保存一个数那应该保存哪个呢这不就冲突了吗
“如果不用这个实实在在的地址,那应该怎么办呢?那就必须用封闭开发的办法。
每个项目的物理地址对于进程不可见谁也不能直接访问这个物理地址。操作系统会给进程分配一个虚拟地址。所有进程看到的这个地址都是一样的里面的内存都是从0开始编号。
在程序里面指令写入的地址是虚拟地址。例如位置为10M的内存区域操作系统会提供一种机制将不同进程的虚拟地址和内存的物理地址映射起来。
当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,就不会冲突了。”
小马想想,对啊,这是个好办法,咱们得规划一套会议室管理系统(内存管理)。根据刚才的分析,这个系统应该包含以下三个部分:
第一,物理内存的管理,相当于会议室管理员管理会议室;
第二,虚拟地址的管理,也即在项目组的视角,会议室的虚拟地址应该如何组织;
第三,虚拟地址和物理地址如何映射的问题,也即会议室管理员如果管理映射表。
我们先来盘点一下物理内存的情况。
<img src="https://static001.geekbang.org/resource/image/8f/49/8f158f58dda94ec04b26200073e15449.jpeg" alt="">
不同的园区工位的安排和会议室的布局各不相同。
第一种情况是CPU小伙伴们坐在一起会议室在楼层的另一面大家到会议室里面去都要通过统一的过道优点简单缺点是通道会成为瓶颈。
第二种情况是会议室分成多个节点离散地分布在CPU小伙伴周围。有的小伙伴离这个会议室近一些有的小伙伴离另外一些会议室近一些。这样做的优点是如果CPU小伙伴干活总是能够去离他最近的会议室则速度非常快但是一旦离他最近的会议室被占用了他只能去其他会议室这样就比较远了。
现在的园区基本都设计成第二种样子也即会议室内存要分节点每个节点用struct pglist_data表示。
每个节点里面再分区域用于区分内存不同部分的不同用法。ZONE_NORMAL是最常用的区域。ZONE_MOVABLE是可移动区域。我们通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。每个区域用struct zone表示也放在一个数组里面。
每个区域里面再分页。默认的大小为4KB。这就相当于每个会议室的最小单位。
如果有项目要使用会议室应该如何分配呢不能任何项目来了咱都给他整个会议室。会议室也是可以再分割的例如在中间拼起一堵墙这样一个会议室就可以分成两个继续分可以再分成四个1/4大小的会议室直到不能再分我们就能得到一页的大小。
物理页面分配的时候,也可以采取这样的思路,我们称为伙伴系统。
空闲页放在struct free_area里面每一页用struct page表示。
把所有的空闲页分组为11个页块链表每个块链表分别包含很多个大小的页块有1、2、4、8、16、32、64、128、256、512和1024个连续页的页块。最大可以申请1024个连续页对应4MB大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。
<img src="https://static001.geekbang.org/resource/image/3f/4f/3fa8123990e5ae2c86859f70a8351f4f.jpeg" alt="">
例如要请求一个128个页的页块时我们要先检查128个页的页块链表是否有空闲块。如果没有则查256个页的页块链表如果有空闲块的话则将256个页的页块分成两份一份使用一份插入128个页的页块链表中。如果还是没有就查512个页的页块链表如果有的话就分裂为128、128、256三个页块一个128的使用剩余两个插入对应页块链表。
把物理页面分成一块一块大小相同的页这样带来的另一个好处是当有的内存页面长时间不用了可以暂时写到硬盘上我们称为换出。一旦需要的时候再加载进来就叫作换入。这样可以扩大可用物理内存的大小提高物理内存的利用率。在内核里面也即张昭的管理下有一个进程kswapd可以根据物理页面的使用情况对页面进行换入换出。
小马觉得这种方式太好了,如此高效地使用会议室,公司不用租用多少会议室,就能解决当前的项目问题了。
## 会议室排列有序,分视角各有洞天
周瑜说,“你先别急,这还仅仅是会议室物理地址的管理,每一个项目组能够看到的虚拟地址,咱还没规划呢!这个规划不好,执行项目还是会有问题的。”
每个项目组能看到的虚拟地址怎么规划呢?我们要给项目组这样一种感觉,从项目组的角度,也即从虚的角度来看,这一大片连续的内存空间都是他们的了。
如果是32位有2^32 = 4G的内存空间都是他们的不管内存是不是真的有4G。如果是64位在x86_64下面其实只使用了48位那也挺恐怖的。48位地址长度也就是对应了256TB的地址空间。
小马说“我都没怎么见过256T的硬盘别说是内存了。”
周瑜接着说:“现在,一个项目组觉得,会议室可比世界首富房子还大。虽然是虚拟的,下面尽情地去排列咱们要放的东西吧!请记住,现在我们是站在一个进程的角度,去看这个虚拟的空间,不用管其他进程。”
首先这么大的虚拟空间一切二一部分用来放内核的东西称为内核空间一部分用来放进程的东西称为用户空间。用户空间在下在低地址我们假设是0号到29号会议室内核空间在上在高地址我们假设是30号到39号会议室。这两部分空间的分界线因为32位和64位的不同而不同我们这里不深究。
对于普通进程来说内核空间的那部分虽然虚拟地址在那里但是不能访问。这就像作为普通员工你明明知道财务办公室在这个30号会议室门里面但是门上挂着“闲人免进”你只能在自己的用户空间里面折腾。
<img src="https://static001.geekbang.org/resource/image/af/83/afa4beefd380effefb0e54a8d9345c83.jpeg" alt="">
我们从最低位开始排起先是Text Segment、Data Segment和BSS Segment。Text Segment是存放二进制可执行代码的位置Data Segment存放静态常量BSS Segment存放未初始化的静态变量。这些都是在项目执行计划书里面有的。
接下来是堆段。堆是往高地址增长的是用来动态分配内存的区域malloc就是在这里面分配的。
接下来的区域是Memory Mapping Segment。这块地址可以用来把文件映射进内存用的如果二进制的执行文件依赖于某个动态链接库就是在这个区域里面将so文件映射到了内存中。
再下面就是栈地址段了,主线程的函数调用的函数栈就是用这里的。
如果普通进程还想进一步访问内核空间,是没办法的,只能眼巴巴地看着。如果需要进行更高权限的工作,就需要调用系统调用,进入内核。
一旦进入了内核就换了一副视角。刚才是普通进程的视角觉着整个空间是它独占的没有其他进程存在。当然另一个进程也这样认为因为它们互相看不到对方。这也就是说不同进程的0号到29号会议室放的东西都不一样。
但是到了内核里面无论是从哪个进程进来的看到的是同一个内核空间看到的是同一个进程列表。虽然内核栈是各用各的但是如果想知道的话还是能够知道每个进程的内核栈在哪里的。所以如果要访问一些公共的数据结构需要进行锁保护。也就是说不同的进程进入到内核后进入的30号到39号会议室是同一批会议室。
<img src="https://static001.geekbang.org/resource/image/4e/9d/4ed91c744220d8b4298237d2ab2eda9d.jpeg" alt="">
内核的代码访问内核的数据结构大部分的情况下都是使用虚拟地址的。虽然内核代码权限很大但是能够使用的虚拟地址范围也只能在内核空间也即内核代码访问内核数据结构只能用30号到39号这些编号不能用0到29号因为这些是被进程空间占用的。而且进程有很多个。你现在在内核但是你不知道当前指的0号是哪个进程的0号。
在内核里面也会有内核的代码同样有Text Segment、Data Segment和BSS Segment内核代码也是ELF格式的。
不过有了这个规定以后,项目执行计划书要写入数据的时候,就需要符合里面的规定了,数据不能随便乱放了。
小马说,“没问题,这个作为项目章程,每一个新员工来了都培训。”
## 管理系统全搞定,至此生存无问题
周瑜接着说:“物理会议室和虚拟空间都分成大小相同的页,我们还得有一个会议室管理系统,将两者关联起来,这样项目组申请会议室的时候,也有个系统可以统一的管理,要不然会议室还不得老冲突呀。”
对于虚拟内存的访问,也是有一个地址的,我们需要找到一种策略,实现从虚拟地址到物理地址的转换。
为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。
<img src="https://static001.geekbang.org/resource/image/ab/40/abbcafe962d93fac976aa26b7fcb7440.jpg" alt="">
虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
下面的图,举了一个简单的页表的例子,虚拟内存中的页通过页表映射对应到物理内存中的页。
<img src="https://static001.geekbang.org/resource/image/83/c3/83a5de160088a2e23e7c1a76c013efc3.jpg" alt="">
32位环境下虚拟地址空间共4GB。如果分成4KB一个页那就是1M个页。每个页表项需要4个字节来存储那么整个4GB空间的映射就需要4MB的内存来存储映射表。如果每个进程都有自己的映射表100个进程就需要400MB的内存。对于内核来讲有点大了 。
页表中所有页表项必须提前建好,并且要求是连续的。如果不连续,就没有办法通过虚拟地址里面的页号找到对应的页表项了。
那怎么办呢我们可以试着将页表再分页4G的空间需要4M的页表来存储映射。我们把这4M分成1K1024个4K每个4K又能放在一页里面这样1K个4K就是1K个页这1K个页也需要一个表进行管理我们称为页目录表这个页目录表里面有1K项每项4个字节页目录表大小也是4K。
页目录有1K项用10位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项也即4K的页表项。每个页表项也是4个字节因而一整页的页表项是1k个。再用10位就可以表示访问页表项的哪一项页表项中的一项对应的就是一个页是存放数据的页这个页的大小是4K用12位可以定位这个页内的任何一个位置。
这样加起来正好32位也就是用前10位定位到页目录表中的一项。将这一项对应的页表取出来共1k项再用中间10位定位到页表中的一项将这一项对应的存放数据的页取出来再用最后12位定位到页中的具体位置访问数据。
<img src="https://static001.geekbang.org/resource/image/b6/b8/b6960eb0a7eea008d33f8e0c4facc8b8.jpg" alt="">
你可能会问如果这样的话映射4GB地址空间就需要4MB+4KB的内存这样不是更大了吗 当然如果页是满的,当时是更大了,但是,我们往往不会为一个进程分配那么多内存。
比如说上面图中我们假设只给这个进程分配了一个数据页。如果只使用页表也需要完整的1M个页表项共4M的内存但是如果使用了页目录页目录需要1K个全部分配占用内存4K但是里面只有一项使用了。到了页表项只需要分配能够管理那个数据页的页表项页就可以了也就是说最多4K这样内存就节省多了。
当然对于64位的系统两级肯定不够了就变成了四级目录分别是全局页目录项PGDPage Global Directory、上层页目录项PUDPage Upper Directory、中间页目录项PMDPage Middle Directory和页表项PTEPage Table Entry
<img src="https://static001.geekbang.org/resource/image/42/0b/42eff3e7574ac8ce2501210e25cd2c0b.jpg" alt="">
设计完毕会议室管理系统再加上前面的项目管理系统对于一家外包公司来讲无论接什么样的项目都能轻松搞定了。我们常把CPU和内存合称为计算。至此计算的问题就算搞定了。解决了这两大问题一家外包公司的生存问题就算解决了。
小马总算是可以松一口气了,他和周瑜、张昭好好地搓了一顿,喝得昏天黑地。周瑜和张昭纷纷感慨,幸亏当年跟了马哥,今日才有出头之日。
生存问题虽然解决了,马哥可非池中之物,接下来要解决的就是发展问题,马哥能想出什么办法进一步壮大企业呢?欲知后事,且听下回分解。

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="65 | 知识串讲:用一个创业故事串起操作系统原理(四)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c3/71/c39a7a5ff135351b1eaf4a74890b1c71.mp3"></audio>
上一节,小马的公司已经解决了生存问题,成功从小马晋升马哥。
马哥是一个有危机意识的人。尽管公司开始不断盈利,项目像流水一样,一个接一个,赚了点儿钱,但是他感觉还是有点儿像狗熊掰棒子。因为公司没有积累,永远就都是在做小生意,无法实现成倍的增长。
马哥想,公司做了这么多的项目,应该有很多的共同点,能积累下来非常多的资料。如果能够把这些资料归档、总结、积累,形成核心竞争力,就可以随着行业的飞跃,深耕一个行业,实现快速增长。
## 公司发展需积累,马哥建立知识库
这就需要我们有一个存放资料的档案库(文件系统)。档案库应该不依赖于项目而独立存在,应该井井有条、利于查询;应该长久保存,不随人员流动而损失。
公司到了这个阶段,除了周瑜和张昭,应该专门请一个能够积累核心竞争力的人来主持大局了。马哥想到了,前一阵行业交流大会上,他遇到了一个很牛的架构师——鲁肃。他感觉鲁肃在这方面很有想法,于是就请他来主持大局。
鲁肃跟马哥说,构建公司的核心技术能力,这个档案库(文件系统)也可以叫作知识库,这个需要好好规划一下。规划文件系统的时候,需要考虑以下几点。
第一点,文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。
这就像图书馆里我们会给设置一排排书架然后再把书架分成一个个小格子。有的项目存放的资料非常多一个格子放不下就需要多个格子来进行存放。我们把这个区域称为存放原始资料的仓库区。对于操作系统硬盘分成相同大小的单元我们称为块。一块的大小是扇区大小的整数倍默认是4K用来存放文件的数据部分。这样一来如果我们像存放一个文件就不用给他分配一块连续的空间了。我们可以分散成一个个小块进行存放。这样就灵活得多也比较容易添加、删除和插入数据。
第二点,文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置。
这就好比,图书馆的书太多了,为了方便查找,我们需要专门设置一排书架,这里面会写清楚整个档案库有哪些资料,资料在哪个架子的哪个格子上。这样找资料的时候就不用跑遍整个档案库,只要在这个书架上找到后,直奔目标书架就可以了。
在Linux操作系统里面每一个文件有一个Inodeinode的“i”是index的意思其实就是“索引”。inode里面有文件的读写权限i_mode属于哪个用户i_uid哪个组i_gid大小是多少i_size_io占用多少个块i_blocks_io。“某个文件分成几块、每一块在哪里”这些信息也在inode里面保存在i_block里面。
<img src="https://static001.geekbang.org/resource/image/93/07/93bf5e8e940752b32531ed6752b5f607.png" alt="">
第三点,如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层。
这就相当于图书馆里面的热门图书区,这里面的书都是畅销书或者是常常被借还的图书。因为借还的次数比较多,那就没必要每次有人还了之后,还放回遥远的货架,我们可以专门开辟一个区域,放置这些借还频次高的图书。这样借还的效率就会提高。
第四点,文件应该用文件夹的形式组织起来,方便管理和查询。
这就像在图书馆里面,你可以给这些资料分门别类,比如分成计算机类、文学类、历史类等等。这样你也容易管理,项目组借阅的时候只要在某个类别中去找就可以了。
在文件系统中,每个文件都有一个名字,我们访问一个文件,希望通过他的名字就可以找到。文件名就是一个普通的文本,所以文件名经常会冲突,不同用户取相同的名字的情况会经常出现的。
要想把很多的文件有序地组织起来,我们就需要把他们做成目录或者文件夹。这样,一个文件夹里可以包含文件夹,也可以包含文件,这样就形成了一种树形结构。我们可以将不同的用户放在不同的用户目录下,就可以一定程度上避免了命名的冲突问题。
<img src="https://static001.geekbang.org/resource/image/e7/4f/e71da53d6e2e4458bcc0af1e23f08e4f.png" alt="">
第五点Linux内核要在自己的内存里面维护一套数据结构来保存哪些文件被哪些进程打开和使用。
这就好比,图书馆里会有个图书管理系统,记录哪些书被借阅了,被谁借阅了,借阅了多久,什么时候归还。
这个图书管理系统尤为重要,如果不是很方便使用,以后项目中积累了经验,就没有人愿意往知识库里面放了。
<img src="https://static001.geekbang.org/resource/image/3c/73/3c506edf93b15341da3db658e9970773.jpg" alt="">
无论哪个项目进程都可以通过write系统调用写入知识库。
对于每一个进程打开的文件都有一个文件描述符。files_struct里面会有文件描述符数组。每个一个文件描述符是这个数组的下标里面的内容指向一个struct file结构表示打开的文件。这个结构里面有这个文件对应的inode最重要的是这个文件对应的操作file_operation。如果操作这个文件就看这个file_operation里面的定义了。
每一个打开的文件都有一个dentry对应虽然我们叫作directory entry但是他不仅仅表示文件夹也表示文件。他最重要的作用就是指向这个文件对应的inode。
如果说file结构是一个文件打开以后才创建的dentry是放在一个dentry cache里面的。文件关闭了他依然存在因而他可以更长期的维护内存中的文件的表示和硬盘上文件的表示之间的关系。
inode结构就表示硬盘上的inode包括块设备号等。这个inode对应的操作保存在inode operations里面。真正写入数据是写入硬盘上的文件系统例如ext4文件系统。
马哥听了知识库和档案库的设计,非常开心,对鲁肃说,你这五大秘籍,可是帮了我大忙了。于是马上下令实施。
## 有了积累建生态,成立渠道管理部
有了知识库,公司的面貌果然大为改观。
马哥发现,当知识库积累到一定程度,公司接任何项目都能找到相似的旧项目作为参考,不用重新设计,效率大大提高。而且最重要的一点是,没有知识库的时候,原来项目做得好不好,完全取决于程序员,因为所有的知识都在程序员的脑子里,所以公司必须要招聘高质量的程序员,才能保证项目的质量。一方面优秀的程序员数量很少,这大大限制了公司能够接项目的规模,一方面优秀的程序员实在太贵,大大提高了公司的成本。
有了知识库,依赖于原来积累的体系,只要找到类似的旧项目,哪怕是普通的程序员,只要会照猫画虎,结果就不会太差。
于是,马哥马上想到,现在公司只有百十来号人,能赚这些钱,现在招人门槛降低了,我要是招聘一万人,这能赚多少钱啊!
鲁肃对马哥说,“你可先别急着招人,建立知识库,降低招人成本才是第一步。公司招聘太多人不容易管理。既然项目的执行可以照猫画虎,很多项目可以不用咱们公司来,我们可以建立渠道销售体系(输入和输出系统),让供应商、渠道帮我们卖,形成一个生态。这公司的盈利规模可就不是招一万人这么点儿了,这是指数级的增长啊!”
<img src="https://static001.geekbang.org/resource/image/80/7f/80e152fe768e3cb4c84be62ad8d6d07f.jpg" alt="">
计算机系统的输入和输出系统都有哪些呢我们能举出来的例如键盘、鼠标、显示器、网卡、硬盘、打印机、CD/DVD等等多种多样。这样当然方便用户使用了但是对于操作系统来讲却是一件复杂的事情因为这么多设备形状、用法、功能都不一样怎么才能统一管理起来呢我们一层一层来看。
第一层,用设备控制器屏蔽设备差异。
马哥说,“把生意做到全国,我也想过,这个可不容易。咱们客户多种多样,众口难调,不同的地域不一样,不同的行业不一样。如果你不懂某个地方的规矩,根本卖不出去东西;如果你不懂某个具体行业的使用场景,也无法满足客户的需求。”
鲁肃说:“所以说,建议您建立生态,设置很多代理商,让各个地区和各个行业的代理商帮你屏蔽这些差异化。你和代理商之间只要进行简单的标准产品交付就可以了。”
计算机系统就是这样的。CPU并不直接和设备打交道他们中间有一个叫作设备控制器Device Control Unit的组件。例如硬盘有磁盘控制器、USB有USB控制器、显示器有视频控制器等。这些控制器就像代理商一样他们知道如何应对硬盘、鼠标、键盘、显示器的行为。
你的代理商往往是小公司。控制器其实有点儿像一台小电脑。他有他的芯片类似小CPU执行自己的逻辑。他也有他的寄存器。这样CPU就可以通过写这些寄存器对控制器下发指令通过读这些寄存器查看控制器对于设备的操作状态。
CPU对于寄存器的读写可比直接控制硬件要标准和轻松很多。这就相当于你和代理商的标准产品交付。
第二层,用驱动程序屏蔽设备控制器差异。
马哥说:“你这么一说,还真有道理,如果我们能够找到足够多的代理商,那就高枕无忧了。”
鲁肃说:“其实事情还没这么简单,虽然代理商机制能够帮我们屏蔽很多设备的细节,但是从上面的描述我们可以看出,由于每种设备的控制器的寄存器、缓冲区等使用模式,指令都不同。对于咱们公司来讲,就需要有个部门专门对接代理商,向其他部门屏蔽代理商的差异,成立公司的渠道管理部门。”
那对于操作系统来讲,渠道管理部门就是用来对接各个设备控制器的设备驱动程序。
这里需要注意的是,设备控制器不属于操作系统的一部分,但是设备驱动程序属于操作系统的一部分。操作系统的内核代码可以像调用本地代码一样调用驱动程序的代码,而驱动程序的代码需要发出特殊的面向设备控制器的指令,才能操作设备控制器。
设备驱动程序中是一些面向特殊设备控制器的代码。不同的设备不同。但是对于操作系统其他部分的代码而言,设备驱动程序应该有统一的接口。就像下面图中的一样,不同的设备驱动程序,可以以同样的方式接入操作系统,而操作系统的其他部分的代码,也可以无视不同设备的区别,以同样的接口调用设备驱动程序。
<img src="https://static001.geekbang.org/resource/image/7b/68/7bf96d3c8e3a82cdac9c7629b81fa368.png" alt="">
第三,用中断控制器统一外部事件处理。
马哥听了恍然大悟:“原来代理商也是五花八门,里面有这么多门道啊!”
鲁肃说:“当咱们对接的代理商多了,代理商可能会有各种各样的问题找到我们,例如代理商有了新客户,客户有了新需求,客户交付完毕等事件,都需要有一种机制通知你们公司,当然是中断,那操作系统就需要有一个地方处理这个中断,既然设备驱动程序是用来对接设备控制器的,中断处理也应该在设备驱动里面完成。”
然而中断的触发最终会到达CPU会中断操作系统当前运行的程序所以操作系统也要有一个统一的流程来处理中断使得不同设备的中断使用统一的流程。
一般的流程是一个设备驱动程序初始化的时候要先注册一个该设备的中断处理函数。咱们讲进程切换的时候说过中断返回的那一刻是进程切换的时机。中断的时候触发的函数是do_IRQ。这个函数是中断处理的统一入口。在这个函数里面我们可以找到设备驱动程序注册的中断处理函数Handler然后执行他进行中断处理。
<img src="https://static001.geekbang.org/resource/image/aa/c0/aa9d074d9819f0eb513e11014a5772c0.jpg" alt="">
第四,用文件系统接口屏蔽驱动程序的差异。
马哥又问了:“对接了这么多代理商,如果咱们内部的工程师要和他们打交道,有没有一种统一的方式呢?”
鲁肃说:“当然应该了,我们内部员工操作外部设备,可以基于文件系统的接口,制定一个统一的标准。”
其实文件系统的机制是一个非常好的机制,咱们公司应该定下这样的规则,一切皆文件。
所有设备都在/dev/文件夹下面创建一个特殊的设备文件。这个设备特殊文件也有inode但是他不关联到硬盘或任何其他存储介质上的数据而是建立了与某个设备驱动程序的连接。
有了文件系统接口之后我们不但可以通过文件系统的命令行操作设备也可以通过程序调用read、write函数像读写文件一样操作设备。
对于块设备来讲在驱动程序之上文件系统之下还需要一层通用设备层。比如咱们讲的文件系统里面的逻辑和磁盘设备没有什么关系可以说是通用的逻辑。在写文件的最底层我们看到了BIO字眼的函数但是好像和设备驱动也没有什么关系。
是的因为块设备类型非常多而Linux操作系统里面一切是文件。我们也不想文件系统以下就直接对接各种各样的块设备驱动程序这样会使得文件系统的复杂度非常高。所以我们在中间加了一层通用块层将与块设备相关的通用逻辑放在这一层维护与设备无关的块的大小然后通用块层下面对接各种各样的驱动程序。
<img src="https://static001.geekbang.org/resource/image/a3/e5/a364f9a9ac045c5d4c1c5a7dfa9ca6e5.png" alt="">
鲁肃帮助马哥建立了这套体系之后,果真业务有了很大起色。原来公司只敢接华东区的项目,毕竟比较近,沟通交付都很方便。后来项目扩展到所有一线城市、二线城市、省会城市,项目数量实现了几十倍的增长。
## 千万项目难度大,集体合作可断金
项目接的多了,就不免有大型的项目,涉及多个行业多个领域,需要多个项目组进行合作才能完成。那两个项目组应该通过什么样的方式,进行沟通与合作呢?作为老板,马哥应该如何设计整个流程呢?
马哥叫来周瑜、张昭、鲁肃,一起商量团队间的合作模式。大家一起献计献策。好在有很多成熟的项目管理流程可以参考。
最最传统的模型就是软件开发的瀑布模型。所谓的瀑布模型,其实就是将整个软件开发过程分成多个阶段,往往是上一个阶段完全做完,才将输出结果交给下一个阶段。这种模型类似进程间通信的管道模型。
所谓的管道,就是在两个进程之间建立一条单向的通道,其实是一段缓存,它会将前一个命令的输出,作为后一个命令的输入。
<img src="https://static001.geekbang.org/resource/image/c0/e2/c042b12de704995e4ba04173e0a304e2.png" alt="">
张昭说,瀑布模型的开发流程效率比较低下,现在大部分公司都不使用这种开发模式了,因为团队之间无法频繁地沟通。而且,管道的使用模式,也不适合进程间频繁的交换数据。
于是,他们还得想其他的办法。是不是可以借鉴传统外企的沟通方式——邮件呢?邮件有一定的格式,例如抬头、正文、附件等。发送邮件可以建立收件人列表,所有在这个列表中的人,都可以反复地在此邮件基础上回复,达到频繁沟通的目的。这个啊,就是消息队列模型。
<img src="https://static001.geekbang.org/resource/image/ac/a4/ac6ad6c9e7e3831f6d813113ae1c5ba4.png" alt="">
和管道将信息一股脑儿地从一个进程,倒给另一个进程不同,消息队列有点儿像邮件,发送数据时,会分成一个一个独立的数据单元,也就是消息体,每个消息体都是固定大小的存储块,在字节流上不连续。
有了消息这种模型,两个进程之间的通信就像咱们平时发邮件一样,你来一封,我回一封,可以频繁沟通了。
<img src="https://static001.geekbang.org/resource/image/df/38/df910e4383885b1aceaafb52b9bb5638.png" alt="">
但是有时候,项目组之间的沟通需要特别紧密,而且要分享一些比较大的数据。如果使用邮件,就发现,一方面邮件的来去不及时;另外一方面,附件大小也有限制,所以,这个时候,我们经常采取的方式就是,把两个项目组在需要合作的期间,拉到一个会议室进行合作开发,这样大家可以直接交流文档呀,架构图呀,直接在白板上画或者直接扔给对方,就可以直接看到。
可以看出来共享会议室这种模型类似进程间通信的共享内存模型。前面咱们讲内存管理的时候知道每个进程都有自己独立的虚拟内存空间不同的进程的虚拟内存空间映射到不同的物理内存中去。这个进程访问A地址和另一个进程访问A地址其实访问的是不同的物理内存地址对于数据的增删查改互不影响。
但是,咱们是不是可以变通一下,拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去。
马哥说:“共享内存也有问题呀。如果两个进程使用同一个共享内存,大家都往里面写东西,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。”
张昭说:“当然,和共享内存配合的,有另一种保护机制,使得同一个共享的资源,同时只能被一个进程访问叫信号量。”
信号量其实是一个计数器,主要用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
我们可以将信号量初始化为一个数值来代表某种资源的总体数量。对于信号量来讲会定义两种原子操作一个是P操作我们称为申请资源操作。这个操作会申请将信号量的数值减去N表示这些数量被他申请使用了其他人不能用了。另一个是V操作我们称为归还资源操作这个操作会申请将信号量加上M表示这些数量已经还给信号量了其他人可以使用了。
例如你有100元钱就可以将信号量设置为100。其中A向你借80元就会调用P操作申请减去80。如果同时B向你借50元但是B的P操作比A晚那就没有办法只好等待A归还钱的时候B的P操作才能成功。之后A调用V操作申请加上30元也就是还给你30元这个时候信号量有50元了这时候B的P操作才能成功才能借走这50元。
所谓原子操作Atomic Operation就是任何一块钱都只能通过P操作借给一个人不能同时借给两个人。也就是说当A的P操作借80和B的P操作借50几乎同时到达的时候不能因为大家都看到账户里有100就都成功必须分个先来后到。
马哥说“有了上面的这些机制基本常规状态下的工作模式对应到咱们平时的工作交接收发邮件、联合开发等。我还想到如果发生了异常怎么办例如出现线上系统故障这个时候什么流程都来不及了不可能发邮件也来不及开会所有的架构师、开发、运维都要被通知紧急出动。所以7乘24小时不间断执行的系统都需要有告警系统一旦出事情就要通知到人哪怕是半夜也要电话叫起来处理故障。是不是应该还有一种异常情况下的工作模式。”
张昭说“当然应该有我们可以建立像操作系统里面的信号机制。信号没有特别复杂的数据结构就是用一个代号一样的数字。Linux提供了几十种信号分别代表不同的意义。信号之间依靠它们的值来区分。这就像咱们看警匪片对于紧急的行动都是说1号作战任务开始执行警察就开始行动了。情况紧急不能啰里啰嗦了。”
信号可以在任何时候发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就可以了。这就相当于咱们运维一个系统应急手册,当遇到什么情况,做什么事情,都事先准备好,出了事情照着做就可以了。
<img src="https://static001.geekbang.org/resource/image/7c/28/7cb86c73b9e73893e6b0e0433d476928.png" alt="">
这些项目组合作的流程设计合理,因而推行起来十分顺畅,现在接个千万级别的项目没有任何问题,根据交易量估值市值,起码有十个亿。
马哥有些小激动,原来自己身价这么高了,是不是也能上个市啥的,实现亿万富翁的梦想呢?于是马哥找了一些投资人聊了聊,投资人说,要想冲一把上市,还差点劲,目前的项目虽然大,但是想象力不够丰富。
那接下来,马哥如何做才能满足市场的想象力,最终成功上市呢?预知后事,且听下回分解。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="66 | 知识串讲:用一个创业故事串起操作系统原理(五)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/8a/7a36609450b6274c4f774e071be3e18a.mp3"></audio>
上一节我们说到,马哥的公司现在接个千万级别的项目没有任何问题,但是投资人说,要想冲一把上市,还差点劲,目前的项目虽然大,但是想象力不够丰富。
## 亿级项目创品牌,战略合作遵协议
马哥突然想到,西部有一个智慧城市的订单,金额几个亿,绝对标杆性质的。如果能够参与其中,应该是很有想象力的事情。
可是,甲方明确地说,“整个智慧城市的建设体系非常的大,一家公司做不下来,需要多家公司合作才能完成。你们有多家公司合作的经验和机制吗?”
马哥咬牙说道:“当然有!”先应下来再说呗,可是这心里是真没底。原来公司都是独自接单,现在要和其他公司合作,协议怎么签,价格怎么谈呢?
马哥找到鲁肃。鲁肃说:“我给你推荐一个人吧!这个人人脉广,项目运作能力强,叫陆逊,说不定能帮上忙。”
鲁肃找来陆逊。陆逊说:“这个好办。公司间合作嘛,就是条款谈好,利益分好就行,关键是大家要遵守行规。大家都按统一的规则来,事情就好办。”
这其实就像机器与机器之间合作,一台机器将自己想要表达的内容,按照某种约定好的格式发送出去。当另外一台机器收到这些信息后,也能够按照约定好的格式解析出来,从而准确、可靠地获得发送方想要表达的内容。这种约定好的格式就是网络协议。
现在业内知名的有两种网络协议模型一种是OSI的标准七层模型一种是业界标准的TCP/IP模型。它们的对应关系如下图所示
<img src="https://static001.geekbang.org/resource/image/92/0e/92f8e85f7b9a9f764c71081b56286e0e.png" alt="">
我们先从第三层网络层开始因为这一层有我们熟悉的IP地址所以这一层我们也叫IP层。
连接到网络上的每一个设备都至少有一个IP地址用于定位这个设备。无论是近在咫尺的、你旁边同学的电脑还是远在天边的电商网站都可以通过IP地址进行定位。因此IP地址类似互联网上的邮寄地址是有全局定位功能的。
就算你要访问美国的一个地址,也可以从你身边的网络出发,通过不断地打听道儿,经过多个网络,最终到达目的地址,和快递员送包裹的过程差不多。打听道儿的协议也在第三层,我们称为路由协议。将网络包从一个网络转发给另一个网络的设备,我们称为路由器。
总而言之第三层干的事情就是网络包从一个起始的IP地址沿着路由协议指的道儿经过多个网络通过多次路由器转发到达目标IP地址。
从第三层我们往下看。第二层是数据链路层。有时候我们简称为二层或者MAC层。所谓MAC就是每个网卡都有的唯一的硬件地址不绝对唯一相对大概率唯一即可类比UUID。这虽然也是一个地址但是这个地址是没有全局定位功能的。
就像给你送外卖的小哥不可能根据手机尾号找到你家但是手机尾号有本地定位功能的只不过这个定位主要靠“吼“。外卖小哥到了你的楼层就开始大喊“尾号xxxx的你外卖到了
MAC地址的定位功能局限在一个网络里面也即同一个网络号下的IP地址之间可以通过MAC进行定位和通信。从IP地址获取MAC地址要通过ARP协议是通过在本地发送广播包也就是“吼”获得的MAC地址。
由于同一个网络内的机器数量有限通过MAC地址的好处就是简单。匹配上MAC地址就接收匹配不上就不接收没有什么所谓路由协议这样复杂的协议。当然坏处就是MAC地址的作用范围不能出本地网络所以一旦跨网络通信虽然IP地址保持不变但是MAC地址每经过一个路由器就要换一次。
所以第二层干的事情,就是网络包在本地网络中的服务器之间定位及通信的机制。
我们再往下看第一层物理层。这一层就是物理设备。例如连着电脑的网线我们能连上的WiFi。
从第三层往上看第四层是传输层这里面有两个著名的协议TCP和UDP。尤其是TCP更是广泛使用在IP层的代码逻辑中仅仅负责数据从一个IP地址发送给另一个IP地址丢包、乱序、重传、拥塞这些IP层都不管。处理这些问题的代码逻辑写在了传输层的TCP协议里面。
我们常说TCP是可靠传输协议也是难为它了。因为从第一层到第三层都不可靠网络包说丢就丢是TCP这一层通过各种编号、重传等机制让本来不可靠的网络对于更上层来讲变得“看起来”可靠。哪有什么应用层的岁月静好只不过是TCP层在负重前行。
传输层再往上就是应用层例如咱们在浏览器里面输入的HTTPJava服务端写的Servlet都是这一层的。
二层到四层都是在Linux内核里面处理的应用层例如浏览器、Nginx、Tomcat都是用户态的。内核里面对于网络包的处理是不区分应用的。
从四层再往上就需要区分网络包发给哪个应用。在传输层的TCP和UDP协议里面都有端口的概念不同的应用监听不同的端口。例如服务端Nginx监听80、Tomcat监听8080再如客户端浏览器监听一个随机端口FTP客户端监听另外一个随机端口。
应用层和内核互通的机制就是通过Socket系统调用。所以经常有人会问Socket属于哪一层其实它哪一层都不属于它属于操作系统的概念而非网络协议分层的概念。
操作系统对于网络协议的实现模式是这样的二到四层的处理代码在内核里面七层的处理代码让应用自己去做。两者需要跨内核态和用户态通信就需要一个系统调用完成这个衔接这就是Socket。
如果公司想要和其他公司沟通我们将请求封装为HTTP协议通过Socket发送到内核。内核的网络协议栈里面在TCP层创建用于维护连接、序列号、重传、拥塞控制的数据结构将HTTP包加上TCP头发送给IP层IP层加上IP头发送给MAC层MAC层加上MAC头从硬件网卡发出去。
最终网络包会被转发到目标服务器它发现MAC地址匹配就将MAC头取下来交给上一层。IP层发现IP地址匹配将IP头取下来交给上一层。TCP层会根据TCP头中的序列号等信息发现它是一个正确的网络包就会将网络包缓存起来等待应用层的读取。
应用层通过Socket监听某个端口因而读取的时候内核会根据TCP头中的端口号将网络包发给相应的应用。
<img src="https://static001.geekbang.org/resource/image/d3/5c/d34e667d1c3340deb8c82a2d44f2a65c.png" alt="">
这样一个大项目中,各个公司都按协议来,别说两家公司合作,二十家也没有问题。
于是陆逊带着马哥,到甲方那里,将自己的方案,以及和其他公司的合作模式讲述清楚。马哥成功入围。
这次参与竞标的公司可不少,马哥公司的竞争力和专业性一点都不差,最后终于拿下了智慧生态合作平台的建设部分。这下不得了,一提马哥的公司,业内无人不知,无人不晓,大家纷纷称呼他为“马总”。
## 公司大了不灵活,鼓励创新有妙招
慢慢地,马总发现,公司大有大的好处,自然也有大的毛病,也就是咱们常见的“大公司病”——不灵活。
这里面的不灵活就像Linux服务器越来越强大的时候无论是计算、网络、存储都越来越牛。例如内存动不动就是百G内存网络设备一个端口的带宽就能有几十G甚至上百G。存储在数据中心至少是PB级别的自然也有不灵活的毛病。
资源大小不灵活有时候我们不需要这么大规格的机器可能只想尝试一下某些新业务申请个4核8G的服务器试一下但是不可能采购这么小规格的机器。无论每个项目需要多大规格的机器公司统一采购就限制几种全部是上面那种大规格的。
资源申请不灵活:规格定死就定死吧,可是每次申请机器都要重新采购,周期很长。
资源复用不灵活反正我需要的资源不多和别人共享一台机器吧这样不同的进程可能会产生冲突例如socket的端口冲突。另外就是别人用过的机器不知道上面做过哪些操作有很多的历史包袱如果重新安装则代价太大。
按说,大事情流程严禁没问题,很多小事情也要被拖累走整个流程,而且很容易出现资源冲突,每天跨部门的协调很累人,历史包袱严重,创新没有办法轻装上阵。
很多公司处理这种问题采取的策略是成立独立的子公司,独立决策,独立运营。这种办法往往会用在创新型的项目上。
Linux也采取了这样的手段就是在物理机上面创建虚拟机。每个虚拟机有自己单独的操作系统、灵活的规格一个命令就能启动起来。每次创建都是新的操作系统很好地解决了上面不灵活的问题。
在物理机上的操作系统看来虚拟机是一个普通的应用他和Excel一样只能运行在用户态。但是对于虚拟机里面的操作系统内核来讲运行在内核态应该有高的权限。
要做到这件事情第一种方式完全虚拟化。其实说白了这是一种“骗人”的方式。虚拟化软件会模拟假的CPU、内存、网络、硬盘给到虚拟机让虚拟机里面的内核自我感觉良好感觉他终于又像个内核了。在Linux上一个叫作qemu的工具可以做到这一点。
qemu向虚拟机里面的客户机操作系统模拟CPU和其他的硬件骗客户机GuestOS认为自己和硬件直接打交道其实是同qemu模拟出来的硬件打交道qemu会将这些指令转译给真正的硬件。由于所有的指令都要从qemu里面过一手因而性能就会比较差。
第二种方式硬件辅助虚拟化。可以使用硬件CPU的Intel-VT和AMD-V技术需要CPU硬件开启这个标志位一般在BIOS里面设置。当确认开始了标志位之后通过内核模块KVMGuestOS的CPU指令将不用经过Qemu转译直接运行大大提高了速度。qemu和KVM融合以后就是qemu-kvm。
<img src="https://static001.geekbang.org/resource/image/f7/22/f748fd6b6b84fa90a1044a92443c3522.png" alt="">
第三种方式称为半虚拟化。对于网络或者硬盘的访问,我们让虚拟机内核加载特殊的驱动,重新定位自己的身份。虚拟机操作系统的内核知道自己不是物理机内核,没那么高的权限。他很可能要和很多虚拟机共享物理资源,所以学会了排队。虚拟机写硬盘其实写的是一个物理机上的文件,那我的写文件的缓存方式是不是可以变一下。我发送网络包,根本就不是发给真正的网络设备,而是给虚拟的设备,我可不可以直接在内存里面拷贝给它,等等等等。
网络半虚拟化方式是virtio_net存储是virtio_blk。客户机需要安装这些半虚拟化驱动。客户机内核知道自己是虚拟机所以会直接把数据发送给半虚拟化设备然后经过特殊处理例如排队、缓存、批量处理等性能优化方式最终发送给真正的硬件。这在一定程度上提高了性能。
有了虚拟化的技术,公司的状态改观了不少,在主要的经营方向之外,公司还推出了很多新的创新方向,都是通过虚拟机创建子公司的方式进行的,例如跨境电商、工业互联网、社交等。一方面,能够享受大公司的支持;一方面,也可以和灵活的创业公司进行竞争。
于是,公司就变成集团公司了。
## 独占鳌头定格局,上市敲钟责任重
随着公司越来越大,钱赚的越来越多,马总的公司慢慢从行业的追随者,变成了领导者。这一方面,让马总觉得“会当凌绝顶,一览众山小”;另一方面,马总也觉得“高处不胜寒”。原来公司总是追着别人跑,产业格局,市场格局从来不用自己操心,只要自己的公司能赚钱就行。现在做了领头羊,马总也就慢慢成了各种政府论坛、产业论坛,甚至国际论坛的座上宾。
穷则独善其身达则兼济天下。马总的决策可能关系到产业的发展、地方的GDP和就业甚至未来的国际竞争力。因此即便是和原来相同的事情现在来做方式和层次都不一样了。
就像对于单台Linux服务器最重要的四种硬件资源是CPU、内存、存储和网络。面对整个数据中心成千上万台机器我们只要重点关注这四种硬件资源就可以了。如果运维数据中心依然像的运维一台台物理机的前辈一样天天关心哪个程序放在了哪台机器上使用多少内存、多少硬盘每台机器总共有多少内存、多少硬盘还剩多少内存和硬盘那头就大了。
对于数据中心我们需要一个调度器将运维人员从指定物理机或者虚拟机的痛苦中解放出来实现对于物理资源的统一管理这就是Kubernetes也就是数据中心的操作系统。
<img src="https://static001.geekbang.org/resource/image/1a/e5/1a8450f1fcda83b75c9ba301ebf9fbe5.jpg" alt="">
对于CPU和内存这两种计算资源的管理我们可以通过Docker技术完成。
容器实现封闭的环境主要要靠两种技术一种是看起来是隔离的技术称为namespace。在每个namespace中的应用看到的都是不同的 IP地址、用户空间、进程ID等。另一种是用起来是隔离的技术称为cgroup即明明整台机器有很多的 CPU、内存但是一个应用只能用其中的一部分。
另外,容器里还有镜像。也就是说,在你焊好集装箱的那一刻,将集装箱的状态保存下来的样子。就像孙悟空说“定!”,集装箱里的状态就被“定”在了那一刻。然后,这一刻的状态会被保存成一系列文件。无论在哪里运行这个镜像,都能完整地还原当时的情况。
通过容器我们可以将CPU和内存资源从大的资源池里面隔离出来并通过镜像技术在数据中心里面实现计算资源的自由漂移。
没有操作系统的时候汇编程序员需要指定程序运行的CPU和内存物理地址。同理数据中心的管理员原来也需要指定程序运行的服务器以及使用的CPU和内存。现在Kubernetes里面有一个调度器Scheduler你只需要告诉它你想运行10个4核8G的Java程序它会自动帮你选择空闲的、有足够资源的服务器去运行这些程序。
对于存储无论是分布式文件系统和分布式块存储需要对接到Kubernetes让Kubernetes管理它们。如何对接呢Kubernetes会提供CSI接口。这是一个标准接口不同的存储可以实现这个接口来对接Kubernetes。是不是特别像设备驱动程序呀操作系统只要定义统一的接口不同的存储设备的驱动实现这些接口就能被操作系统使用了。
对于网络也是类似的机制Kubernetes同样是提供统一的接口CNI。无论你用哪种方式实现网络模型只要对接这个统一的接口Kubernetes就可以管理容器的网络。
到此,有了这套鼎定市场格局的策略,作为龙头企业,马总的公司终于可以顺利上市敲钟,走向人生巅峰。从此,江湖人称“马爸爸”。
好了,马同学的创业故事就讲到这里了,操作系统的原理也给你串了一遍。你是否真的记住了这些原理呢?试着将这个创业故事讲给你的朋友听吧!
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,112 @@
<audio id="audio" title="期末测试 | 这些操作系统问题,你真的掌握了吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/96/5d/96ff6d500b5fa98ae05a9ad84300295d.mp3"></audio>
你好,我是刘超。当你看到这篇文章的时候,说明你已经历经九九八十一难,完成了整个课程的学习,你是否已经悟到操作系统的“真谛”了呢?我们今天就来测试一下。
先和你说一下,期末测试题的设计,我采取了和入学测验不一样的思路。当时入学测验里的题目比较细节,类似你在大学里学完操作系统课之后的考试题目。现在整个专栏学完了,我们需要换一种检测方式。所以,期末测试题,我设计成了选择题+开放式问题的形式。这才是我们现实生活中,不管是面试还是工作中,常常会遇到的“考试”方式。
有人说,面试的时候问操作系统知识,就是“面试造航母,上班拧螺丝”,没有啥用。其实不然,这些看似简单开放的问题,其实最能检验真本事的。面试官毕竟阅人无数,带人无数,看似他只是在问你这个问题的答案,其实这些问题背后都有实际的工作场景——只是你可能不了解或者想象不到而已。
因此我把专栏内容中最核心的知识点总结成20道选择题并且把每个部分面试可能会考到的问题也总结成11道开放式面试题。你可不要小看这些面试题也不要以为面试官真的只是单纯地在问你这些问题的答案。因为这些面试题都产生于真实的工作场景中。
在这个场景中,秃头哥因为工作经验丰富,发际线已经非常可怜。长发哥是新进入公司的员工。你可以看看,他们在工作中是怎么互动的,感受一下长发哥回答不出问题的“尴尬”。
同时,你可以想一想,如果换作是你,在面试或者工作中遇到这些问题,你会怎么回答呢?你可以把思考之后的答案写在留言里。当然,如果你的朋友也在为面试或者操作系统知识烦忧,你可以把这篇文章分享给他。
这里呢我还给你准备了几个回答问题的小Tips。你在答题的时候可以参照下面这三个Tips组织你的答案。
**第一,回答尽量体系化。**不仅仅回答这个问题的知识点,还可以简单描述一下这个点背后完整的体系,然后根据面试官接下来的追问,更详细地描述其他相关的内容。
这样做的好处是:一,防止你因为这个点没有回答好而丢掉所有的分;二,可以体现你的知识掌握比较全面,可以加分;三,还可以避免冷场和尬聊。
面试官开放式地提问,最不想听到的就是“封闭式”回答。有的面试者“惜字如金”,有的人能聊一个小时,有的人不到半个小时就答完了,让面试官没有办法深入全面地了解你。
**第二,在体系化的阐述过程中,可以加入一些你使用过的相关工具或者技巧的阐述。**例如操作系统干这个事情有十个步骤。第三步会在某个日志文件里面打印这样的日志如果发现打印出xxx说明可能有错误第七步可以通过某个命令行查看某某状态等等。诸如此类可以让面试官感觉到你是实战派而非理论派。
**第三,在体系化的阐述过程中,如果能加入一些项目经验就更好了。**例如,在第九个步骤,当时在做某项目的时候,因为客户现场的版本问题,导致了什么结果,最后如何进行解决的等等。
我刚才说了,面试官问问题的时候,脑子里都是有实际工作场景的。如果你能把他带到你的工作场景里面,有利于面试官对你当时的情形感同身受,你的分数自然不会很低。
看我说了这么多,你是不是已经迫不及待的想要答题啦?别着急,我们来说说这两套题的答题顺序,我建议你先做选择题,再做面试题,因为这更像我们实际的面试过程,先笔试、再面试。
## 选择题
好了,现在开始答题吧!
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=152&amp;exam_id=337)
## 面试题
### 1.你能说一下操作系统的启动和初始化过程吗?
秃头哥:给虚拟机做一个镜像,让系统起来以后做一些初始化。<br>
长发哥:才初始化,怎么连进去呢?<br>
秃头哥:……
### 2.请问进程和线程的概念和区别是什么?
秃头哥把Dubbo线程池调大一点<br>
长发哥我调到10000了。线程越多是不是性能越高啊<br>
秃头哥:……
### 3.请问函数调用堆栈的原理是什么?
秃头哥:这个接口调用性能比较差,排查一下哪里慢!<br>
长发哥:好的,一定认真排查。<br>
秃头哥:怎么样了?有思路了吗?<br>
长发哥:正在一行行看呢!<br>
秃头哥:……
### 4.对内存管理了解吗?请说一下物理内存和虚拟内存的概念。
秃头哥你看这个进程GC这么严重看看问题在哪儿<br>
长发哥:这么多变量,看哪个?<br>
秃头哥:……
### 5.请介绍一下虚拟文件系统的机制。
长发哥:这个程序运行时间一长就挂了,咋回事呀?<br>
秃头哥:这不有异常吗?超过最大打开文件数了。<br>
长发哥:打开文件还有限制?<br>
秃头哥:……
### 6.你了解文件写入的流程吗?
秃头哥咱们的消息队列模型要慢慢换成Kafka<br>
长发哥我大概瞄了一下Kafka的消息存储是基于硬盘的。这么慢的方式怎么能用<br>
秃头哥:……
### 7.进程间通信的管道机制了解吗?
秃头哥:帮我把这些监控数据里面的第三列拿出来,取一个平均数!<br>
长发哥好的等我打开Excel。<br>
秃头哥:……
### 8.请讲一下信号和中断机制。
秃头哥:为了让这个进程能够优雅的关闭,要给他发一个信号!<br>
长发哥:怎么发信号?<br>
秃头哥kill呀<br>
长发哥:那不就强制干掉了吗?没法儿优雅呀?<br>
秃头哥:……
### 9.请讲一下TCP/IP的分层模型。
长发哥客户要求数据一个不能丢是不是要通过可靠的协议TCP才行<br>
秃头哥当然应该TCP呀<br>
长发哥可是客户还要求数据一个不能多TCP重传可能会导致数据重复啊<br>
秃头哥:……
### 10.请讲一下三次握手和四次挥手以及状态转换。
长发哥:进程明明启动了,为啥连不上呀?<br>
秃头哥:看一下有没有监听端口,处于什么状态!<br>
长发哥:我看配置文件了,端口配置得没错呀。<br>
秃头哥:你看监听的端口和配置的不一样,是不是配置文件有冲突了?<br>
长发哥你真牛你怎么知道另一个jar里面还有一个配置文件<br>
秃头哥:……
### 11.最新的云或者容器的技术了解过吗?
秃头哥客户有一台老的centOS 6需要写一个脚本能在上面安装软件<br>
长发哥:去哪里找这么老的系统啊,帮我申请一台物理机,我找找老镜像装一个。<br>
秃头哥:……

View File

@@ -0,0 +1,85 @@
<audio id="audio" title="结束语 | 永远别轻视任何技术,也永远别轻视自己" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/7e/6c567138ea49bf4dafc62386c1a16b7e.mp3"></audio>
你好我是刘超。又一次时隔5个多月“趣谈Linux操作系统”专栏终于结束了。
之所以说“终于”二字是因为这门课实在是太硬核了。写作的过程中几乎每篇文章都超长。极客时间的要求是每篇3000字左右而这个专栏差不多每篇6000字左右。我之前规划好的很多主题本来写一篇最后都变成了上、中、下三篇。最终在我十分“搂着”的情况下这个专栏从最初计划的52篇扩展到67篇。
说实话,写完这个专栏,我觉得我自身实现了一定的升华,这其中真的产生了非常多的感悟,我在这里跟你分享一下。
## 第一,永远敬畏技术,坚持不懈,持续深耕
当极客时间想让我再写一个基础知识专栏的时候我很快就想到了Linux操作系统。
毕竟我平时几乎天天和Linux操作系统打交道。安装、运维、调优从操作到内核原理从来没有放下过按理来说写一个专栏趣谈一下应该不是问题。
于是,我很快构建了一个创业公司的故事大纲,和编辑做好了课程设计,写出了各节的标题,觉得不用太长,就能写清楚。刚开始写前几篇的时候,还没有涉及内核代码的解读,感觉一切可控,该说原理说原理,该讲故事讲故事。
可是,真的到了后面硬核的内核代码部分,我发现,写起来和想起来完全是两码事。
我个人特别喜欢读优秀的开源软件代码,从中可以学习原理。我一度认为,只要是给我代码看,我还能搞不定原理?毕竟自己原来也跟过内核代码的流程,写专栏按说应该驾轻就熟。但是我发现,内核代码的变化超出我的想象,我自己也迷失在代码的汪洋大海里面了……
内核代码分析特别像走迷宫通关有时候你觉得自己进入了一个房间左看右看也就这么多内容了刚刚欣喜一把就会突然发现角落里有个门打开以后很可能是一个更大的房间。最终就算通了关你也无法保证你能够看到整个全貌。这不由得让我对Linux内核更加敬畏也对技术更加敬畏。
任何一个开源软件,以当前的快速迭代速度,如果三年没碰,肯定面目全非;五年没碰,你就当自己不懂就行了。所以,对于开源软件,千万不要当下能用就好,完全不管原理。那些面试官问你开源软件背后的机制,这不是故意刁难,也不是“面试造航母,上班拧螺丝”,因为在大规模复杂场景下,无论如何重视基座的稳定,都是不过分的。
永远敬畏技术,别轻视技术,你轻视它。它就会静静地看着你,直到某一个时刻给你当头一棒,而且,这一刻来得越晚,这一棒就会打得越狠,打得你爬不起来。
在工作中,当架构的系统因为长期忽略技术被“打倒”的时候,很多人期盼有一个电视里面的老中医,哪怕你一生都不爱惜身体,他也能一副药妙手回春。其实世界上哪有风清扬,令狐冲走投无路,被他指点几招就能秒杀田伯光,做技术要像郭靖练习功夫,先练个十八年马步,再一掌一掌地劈下去。
## 第二,对自己狠一点,发现还是有潜力可挖
写这个专栏虽然很辛苦,但是,我很庆幸,自己还是咬着牙完成了。我不敢保证这个专栏一定是最全面的,也不敢保证里面的一定毫无错漏,但是我敢说,专栏每一节的论述都是有佐证和凭据的。因为除了看代码,我还看了大量的参考书。我在云盘里专门建立了一个单独的文件夹,里面放了我平时写专栏的参考资料,方便随时随地查询。
有的时候,弄清楚一个知识点的内容,就像打开迷宫中一扇门,里面不是一个房间,而是一片草原,需要看几本书才能搞定。那怎么办呢?不服就干呗!
于是磕磕绊绊坚持写完整个专栏。当我再次打开那个文件夹的时候我发现我竟然看了这么多书。数了一下总共32本书。我在这里列一下骄傲一把。
```
《自己动手写操作系统》
《UNIX 环境高级编程》
《一个操作系统的实现》
《系统虚拟化原理与实现》
《深入理解Linux虚拟内存管理》
《深入理解Linux内核》
《深入Linux内核架构》
《穿越计算机的迷雾》
《程序员的自我修养:链接、装载与库》
《操作系统真象还原》
《操作系统设计与实现》
《x86汇编语言从实模式到保护模式》
《linux内核设计的艺术图解》
《Linux设备驱动开发详解》
《Linux内核完全注释》
《Linux内核设计与实现》
《Linux多线程服务端编程》
《Linux 内核分析及编程》
《IBM PC汇编语言程序设计》
《深入理解计算机系统》
《性能之巅:洞悉系统、企业与云计算》
《Linux内核协议栈源代码解析》
《UNIX网络编程》
《Linux/UNIX系统编程手册》
《深入Linux设备驱动程序内核机制》
《深入理解Linux驱动程序设计》
《Linux Device Drivers》
《TCP/IP详解卷》
《The TCP/IP Guide》
《深入理解LINUX网络技术内幕》
《Linux内核源代码情景分析》
《UNIX/Linux系统管理技术手册》
```
现在如果你问我,操作系统这么多人都在讲,学你这个专栏还有啥用,我可以自豪地说,我没有做啥“原创”的事情。你也同样可以选择将上面的书看完,然后对照着自己去解读最新的代码。你自己想想,觉得哪个选择更好呢?
很多读者留言说,读这个专栏非常吃力,难以坚持下去。我想说的是,别轻言放弃,逼自己一把,就像我逼自己咬牙看上面的那些书一样,你会发现自己潜力无穷。
当然,“逼”自己,也是要讲究方法的。如果你基础比较好,你可以在上下班路上听一听,作为复习巩固。对于真正解析流程和数据结构的那些章节,建议你还是坐下来拿着笔边记边读;对于里面的程序,还有课后练习,也不要偷懒,建议你全部做一下,才会有上手的感觉。
一遍看不懂,那就多看几遍。我这里推荐一种方式,你可以先从头到尾看一遍,看到总结的部分,将总结的图拿出来,每一节都打印成一张纸,对着流程从头再看一遍,最后把这些纸订成一个手册,这样就会好很多。
每当你坚持不下去的时候你可以想一想这门课一共60节课也就60个图啃下来这些就能掌握操作系统也不算难吧
你看,我都逼了自己一把,激发出来了一点潜力,写完了这个巨硬核的专栏,你要不要也逼自己一把呢?加油啊,愿我们能一起每日精进!
最后,我在这里放了一个[毕业调查问卷](https://jinshuju.net/f/t3epe1)。如果你对这个专栏或者我本人有什么建议,可以通过这个问卷进行反馈,我一定会认真查看每一封的内容。期待你的反馈!