mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-10 19:54:28 +08:00
del
This commit is contained in:
382
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔.md
Normal file
382
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔.md
Normal 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文件,这样容易维护。
|
||||
|
||||
```
|
||||
<domain type='qemu'>
|
||||
<name>ubuntutest</name>
|
||||
<uuid>0f0806ab-531d-6134-5def-c5b4955292aa</uuid>
|
||||
<memory unit='GiB'>4</memory>
|
||||
<currentMemory unit='GiB'>4</currentMemory>
|
||||
<vcpu placement='static'>2</vcpu>
|
||||
<os>
|
||||
<type arch='x86_64' machine='pc-i440fx-trusty'>hvm</type>
|
||||
<boot dev='hd'/>
|
||||
</os>
|
||||
<features>
|
||||
<acpi/>
|
||||
<apic/>
|
||||
<pae/>
|
||||
</features>
|
||||
<clock offset='utc'/>
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>restart</on_reboot>
|
||||
<on_crash>restart</on_crash>
|
||||
<devices>
|
||||
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||||
<disk type='file' device='disk'>
|
||||
<driver name='qemu' type='qcow2'/>
|
||||
<source file='/mnt/vdc/ubuntutest.img'/>
|
||||
<target dev='vda' bus='virtio'/>
|
||||
</disk>
|
||||
<controller type='pci' index='0' model='pci-root'/>
|
||||
<interface type='bridge'>
|
||||
<mac address='fa:16:3e:6e:89:ce'/>
|
||||
<source bridge='br0'/>
|
||||
<target dev='tap1'/>
|
||||
<model type='virtio'/>
|
||||
</interface>
|
||||
<serial type='pty'>
|
||||
<target port='0'/>
|
||||
</serial>
|
||||
<console type='pty'>
|
||||
<target type='serial' port='0'/>
|
||||
</console>
|
||||
<graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'>
|
||||
<listen type='address' address='0.0.0.0'/>
|
||||
</graphics>
|
||||
<video>
|
||||
<model type='cirrus'/>
|
||||
</video>
|
||||
</devices>
|
||||
</domain>
|
||||
|
||||
```
|
||||
|
||||
在这个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 >= 512){
|
||||
return -1;
|
||||
}
|
||||
copy_from_user(buffer, words, count);
|
||||
ret=printk("User Mode says %s to the Kernel Mode!", 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 --->
|
||||
Compile-time checks and compiler options --->
|
||||
[*] Compile the kernel with debug info
|
||||
[*] Compile the kernel with frame pointers
|
||||
|
||||
```
|
||||
|
||||
选择完毕之后,配置会保存在.config文件中。如果我们打开看,能看到这样的配置:
|
||||
|
||||
```
|
||||
CONFIG_FRAME_POINTER=y
|
||||
CONFIG_DEBUG_INFO=y
|
||||
|
||||
```
|
||||
|
||||
接下来,我们编译内核。
|
||||
|
||||
```
|
||||
nohup make -j8 > make1.log 2>&1 &
|
||||
nohup make modules_install > make2.log 2>&1 &
|
||||
nohup make install > make3.log 2>&1 &
|
||||
|
||||
```
|
||||
|
||||
这是一个非常长的过程,请耐心等待,可能需要数个小时,因而这里用了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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <string.h>
|
||||
|
||||
int main ()
|
||||
{
|
||||
char * words = "I am liuchao from user mode.";
|
||||
int ret;
|
||||
ret = syscall(333, words, strlen(words)+1);
|
||||
printf("return %d from kernel mode.\n", 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内核,添加了一个自己的系统调用,并且进行了编译并安装了新内核。如果你按照这个过程做下来,你会惊喜地发现,原来令我们敬畏的内核,也是能够加以干预,为我而用的呢。没错,这就是你开始逐渐掌握内核的重要一步。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
这一节的课堂练习,希望你能够按照整个过程,一步一步操作下来。毕竟看懂不算懂,做出来才算入门啊。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
|
||||
|
||||
|
||||
287
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔.md
Normal file
287
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔.md
Normal 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 <stdio.h>
|
||||
2 #include <stdlib.h>
|
||||
3 #include <unistd.h>
|
||||
4 #include <linux/kernel.h>
|
||||
5 #include <sys/syscall.h>
|
||||
6 #include <string.h>
|
||||
7
|
||||
8 int main ()
|
||||
9 {
|
||||
10 char * words = "I am liuchao from user mode.";
|
||||
(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 = "I am liuchao from user mode.";
|
||||
(gdb) n
|
||||
12 ret = syscall(333, words, strlen(words)+1);
|
||||
(gdb) p words
|
||||
$1 = 0x5555555547c4 "I am liuchao from user mode."
|
||||
(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的namespace,qemu:commandline这个参数libvirt不认。
|
||||
|
||||
```
|
||||
<domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
|
||||
<name>ubuntutest</name>
|
||||
<uuid>0f0806ab-531d-6134-5def-c5b4955292aa</uuid>
|
||||
<memory unit='KiB'>8388608</memory>
|
||||
<currentMemory unit='KiB'>8388608</currentMemory>
|
||||
<vcpu placement='static'>8</vcpu>
|
||||
<os>
|
||||
<type arch='x86_64' machine='pc-i440fx-trusty'>hvm</type>
|
||||
<boot dev='hd'/>
|
||||
</os>
|
||||
<clock offset='utc'/>
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>restart</on_reboot>
|
||||
<on_crash>restart</on_crash>
|
||||
<devices>
|
||||
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||||
<disk type='file' device='disk'>
|
||||
<driver name='qemu' type='qcow2'/>
|
||||
<source file='/mnt/vdc/ubuntutest.img'/>
|
||||
<backingStore/>
|
||||
<target dev='vda' bus='virtio'/>
|
||||
<alias name='virtio-disk0'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
|
||||
</disk>
|
||||
......
|
||||
<interface type='bridge'>
|
||||
<mac address='fa:16:3e:6e:89:ce'/>
|
||||
<source bridge='br0'/>
|
||||
<target dev='tap1'/>
|
||||
<model type='virtio'/>
|
||||
<alias name='net0'/>
|
||||
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
|
||||
</interface>
|
||||
......
|
||||
</devices>
|
||||
<qemu:commandline>
|
||||
<qemu:arg value='-s'/>
|
||||
</qemu:commandline>
|
||||
</domain>
|
||||
|
||||
```
|
||||
|
||||
另外,为了远程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 "I am liuchao from user mode.", count=29) at kernel/sys.c:192
|
||||
192 {
|
||||
(gdb) bt
|
||||
#0 sys_sayhelloworld (words=0x55b2811537c4 "I am liuchao from user mode.", 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 >= 1024){
|
||||
(gdb) n
|
||||
198 copy_from_user(buffer, words, count);
|
||||
(gdb) n
|
||||
199 ret=printk("User Mode says %s to the Kernel Mode!", buffer);
|
||||
(gdb) p buffer
|
||||
$1 = "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=<optimized out>) 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="">
|
||||
127
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/62 | 知识串讲:用一个创业故事串起操作系统原理(一).md
Normal file
127
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/62 | 知识串讲:用一个创业故事串起操作系统原理(一).md
Normal 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的大小,我们通常称为MBR(Master Boot Record,主引导记录/扇区)。这里保存了boot.img,BIOS手册会将他加载到内存中的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="">
|
||||
136
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/63 | 知识串讲:用一个创业故事串起操作系统原理(二).md
Normal file
136
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/63 | 知识串讲:用一个创业故事串起操作系统原理(二).md
Normal 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="">
|
||||
149
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/64 | 知识串讲:用一个创业故事串起操作系统原理(三).md
Normal file
149
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/64 | 知识串讲:用一个创业故事串起操作系统原理(三).md
Normal 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分成1K(1024)个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位的系统,两级肯定不够了,就变成了四级目录,分别是全局页目录项PGD(Page Global Directory)、上层页目录项PUD(Page Upper Directory)、中间页目录项PMD(Page Middle Directory)和页表项PTE(Page Table Entry)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/0b/42eff3e7574ac8ce2501210e25cd2c0b.jpg" alt="">
|
||||
|
||||
设计完毕会议室管理系统,再加上前面的项目管理系统,对于一家外包公司来讲,无论接什么样的项目都能轻松搞定了。我们常把CPU和内存合称为计算。至此,计算的问题就算搞定了。解决了这两大问题,一家外包公司的生存问题,就算解决了。
|
||||
|
||||
小马总算是可以松一口气了,他和周瑜、张昭好好地搓了一顿,喝得昏天黑地。周瑜和张昭纷纷感慨,幸亏当年跟了马哥,今日才有出头之日。
|
||||
|
||||
生存问题虽然解决了,马哥可非池中之物,接下来要解决的就是发展问题,马哥能想出什么办法进一步壮大企业呢?欲知后事,且听下回分解。
|
||||
193
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/65 | 知识串讲:用一个创业故事串起操作系统原理(四).md
Normal file
193
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/65 | 知识串讲:用一个创业故事串起操作系统原理(四).md
Normal 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操作系统里面,每一个文件有一个Inode,inode的“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="">
|
||||
137
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/66 | 知识串讲:用一个创业故事串起操作系统原理(五).md
Normal file
137
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/66 | 知识串讲:用一个创业故事串起操作系统原理(五).md
Normal 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层在负重前行。
|
||||
|
||||
传输层再往上就是应用层,例如,咱们在浏览器里面输入的HTTP,Java服务端写的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里面设置)。当确认开始了标志位之后,通过内核模块KVM,GuestOS的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="">
|
||||
112
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/期末测试 | 这些操作系统问题,你真的掌握了吗?.md
Normal file
112
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/期末测试 | 这些操作系统问题,你真的掌握了吗?.md
Normal 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&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>
|
||||
秃头哥:……
|
||||
85
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/结束语 | 永远别轻视任何技术,也永远别轻视自己.md
Normal file
85
极客时间专栏/geek/趣谈Linux操作系统/实战串讲篇/结束语 | 永远别轻视任何技术,也永远别轻视自己.md
Normal 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)。如果你对这个专栏或者我本人有什么建议,可以通过这个问卷进行反馈,我一定会认真查看每一封的内容。期待你的反馈!
|
||||
Reference in New Issue
Block a user