目录

Operating System Chapter11 操作系统上的进程


Operating System

$Nanjing\ University\rightarrow Yanyan\ Jiang\newline$

Overview

  • 复习

    • 操作系统内核的启动:CPU ResetFirmwareBoot loaderKernel _start() → …
  • 本次课回答的问题

    • Q1: 操作系统启动后到底做了什么?

    • Q2: 操作系统如何管理程序 (进程)?

  • 本次课主要内容

    • 虚拟化:操作系统上的进程

    • 进程管理 API

操作系统启动后到底做了什么?

从系统启动到第一个进程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <am.h>
#include <klib.h>
#include <klib-macros.h>

#define MAX_CPU 8

typedef union task {
  struct {
    const char *name;
    union task *next;
    void      (*entry)(void *);
    Context    *context;
  };
  uint8_t stack[8192];
} Task;

Task *currents[MAX_CPU];
#define current currents[cpu_current()]

// user-defined tasks

int locked = 0;
void lock()   { while (atomic_xchg(&locked, 1)); }
void unlock() { atomic_xchg(&locked, 0); }

void func(void *arg) {
  while (1) {
    lock();
    printf("Thread-%s on CPU #%d\n", arg, cpu_current());
    unlock();
    for (int volatile i = 0; i < 100000; i++) ;
  }
}

Task tasks[] = {
  { .name = "A", .entry = func },
  { .name = "B", .entry = func },
  { .name = "C", .entry = func },
  { .name = "D", .entry = func },
  { .name = "E", .entry = func },
};

// ------------------

Context *on_interrupt(Event ev, Context *ctx) {
  extern Task tasks[];
  if (!current) current = &tasks[0];
  else          current->context = ctx;
  do {
    current = current->next;
  } while ((current - tasks) % cpu_count() != cpu_current());
  return current->context;
}

void mp_entry() {
  iset(true);
  yield();
}

int main() {
  cte_init(on_interrupt);

  for (int i = 0; i < LENGTH(tasks); i++) {
    Task *task    = &tasks[i];
    Area stack    = (Area) { &task->context + 1, task + 1 };
    task->context = kcontext(stack, task->entry, (void *)task->name);
    task->next    = &tasks[(i + 1) % LENGTH(tasks)];
  }
  mpe_init(mp_entry);
}
  • 操作系统会加载 “第一个程序”
    • RTFSC (latest Linux Kernel)
    1
    2
    3
    4
    5
    6
    7
    
    if (!try_to_run_init_process("/sbin/init") ||
          !try_to_run_init_process("/etc/init") ||
          !try_to_run_init_process("/bin/init") ||
          !try_to_run_init_process("/bin/sh"))
      	return 0;
      panic("No working init found.  Try passing init= option to kernel. "
    	      "See Linux Documentation/admin-guide/init.rst for guidance.");
    
    • $\uparrow$如果没有指定启动选项 init=,按照 “默认列表” 尝试一遍,如果都不行,内核就拒绝启动(panic的部分)
    • 从此以后,Linux Kernel 就进入后台,成为 “中断/异常处理程序”
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ pstree             
init(Ubuntu-22.─┬─SessionLeader───Relay(55)───sh───sh───sh───node─┬─node─┬─zsh───pstree
                │                                                 │      └─11*[{node}]
                │                                                 ├─node─┬─clangd.main───9*[{clangd.main}]
                │                                                 │      ├─node───6*[{node}]
                │                                                 │      ├─node───7*[{node}]
                │                                                 │      └─11*[{node}]
                │                                                 ├─node───12*[{node}]
                │                                                 └─10*[{node}]
                ├─SessionLeader───Relay(486)───node───6*[{node}]
                ├─SessionLeader───Relay(495)───node───6*[{node}]
                ├─init───{init}
                └─{init(Ubuntu-22.}
  • $\uparrow$上面是wsl的,和真机启动方式不太一样
  • 找了一台Ubuntu虚拟机,这回一样了
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
systemd─┬─ModemManager───2*[{ModemManager}]
        ├─NetworkManager───2*[{NetworkManager}]
        ├─ToDesk_Service───13*[{ToDesk_Service}]
        ├─VGAuthService
        ├─accounts-daemon───2*[{accounts-daemon}]
        ├─acpid
        ├─anacron
        ├─avahi-daemon───avahi-daemon
        ├─bluetoothd
        ├─colord───2*[{colord}]
        ├─cron
        ├─cups-browsed───2*[{cups-browsed}]
        ├─cupsd
        ├─dbus-daemon
        ├─dockerd─┬─containerd───7*[{containerd}]
        │         └─8*[{dockerd}]
        ├─fwupd───4*[{fwupd}]
        ├─gdm3─┬─gdm-session-wor─┬─gdm-wayland-ses─┬─gnome-session-b───2*[{gnome-session-b}]
        │      │                 │                 └─2*[{gdm-wayland-ses}]
        │      │                 └─2*[{gdm-session-wor}]
        │      └─2*[{gdm3}]
        ├─geoclue───2*[{geoclue}]
        ├─gnome-keyring-d───3*[{gnome-keyring-d}]
        ├─irqbalance───{irqbalance}
        ├─2*[kerneloops]
        ├─networkd-dispat
        ├─packagekitd───2*[{packagekitd}]
        ├─polkitd───2*[{polkitd}]
        ├─power-profiles-───2*[{power-profiles-}]
        ├─rsyslogd───3*[{rsyslogd}]
        ├─rtkit-daemon───2*[{rtkit-daemon}]
        ├─snapd───12*[{snapd}]
        ├─switcheroo-cont───2*[{switcheroo-cont}]
        ├─systemd─┬─(sd-pam)
        │         ├─at-spi2-registr───2*[{at-spi2-registr}]
        │         ├─dbus-daemon
        │         ├─dconf-service───2*[{dconf-service}]
        │         ├─evolution-addre───5*[{evolution-addre}]
        │         ├─evolution-calen───8*[{evolution-calen}]
        │         ├─evolution-sourc───3*[{evolution-sourc}]
        │         ├─2*[gjs───4*[{gjs}]]
        │         ├─gnome-session-b─┬─at-spi-bus-laun─┬─dbus-daemon
        │         │                 │                 └─3*[{at-spi-bus-laun}]
        │         │                 ├─evolution-alarm───5*[{evolution-alarm}]
        │         │                 ├─gsd-disk-utilit───2*[{gsd-disk-utilit}]
        │         │                 ├─update-notifier───3*[{update-notifier}]
        │         │                 └─3*[{gnome-session-b}]
        │         ├─gnome-session-c───{gnome-session-c}
        │         ├─gnome-shell─┬─Xwayland
        │         │             ├─gjs───5*[{gjs}]
        │         │             └─9*[{gnome-shell}]
        │         ├─gnome-shell-cal───5*[{gnome-shell-cal}]
        │         ├─gnome-terminal-─┬─bash───pstree
        │         │                 └─3*[{gnome-terminal-}]
        │         ├─goa-daemon───3*[{goa-daemon}]
        │         ├─goa-identity-se───2*[{goa-identity-se}]
        │         ├─gsd-a11y-settin───3*[{gsd-a11y-settin}]
        │         ├─gsd-color───3*[{gsd-color}]
        │         ├─gsd-datetime───3*[{gsd-datetime}]
        │         ├─gsd-housekeepin───3*[{gsd-housekeepin}]
        │         ├─gsd-keyboard───3*[{gsd-keyboard}]
        │         ├─gsd-media-keys───3*[{gsd-media-keys}]
        │         ├─gsd-power───3*[{gsd-power}]
        │         ├─gsd-print-notif───2*[{gsd-print-notif}]
        │         ├─gsd-printer───2*[{gsd-printer}]
        │         ├─gsd-rfkill───2*[{gsd-rfkill}]
        │         ├─gsd-screensaver───2*[{gsd-screensaver}]
        │         ├─gsd-sharing───3*[{gsd-sharing}]
        │         ├─gsd-smartcard───3*[{gsd-smartcard}]
        │         ├─gsd-sound───3*[{gsd-sound}]
        │         ├─gsd-wacom───3*[{gsd-wacom}]
        │         ├─gsd-xsettings───3*[{gsd-xsettings}]
        │         ├─gvfs-afc-volume───3*[{gvfs-afc-volume}]
        │         ├─gvfs-goa-volume───2*[{gvfs-goa-volume}]
        │         ├─gvfs-gphoto2-vo───2*[{gvfs-gphoto2-vo}]
        │         ├─gvfs-mtp-volume───2*[{gvfs-mtp-volume}]
        │         ├─gvfs-udisks2-vo───3*[{gvfs-udisks2-vo}]
        │         ├─gvfsd─┬─gvfsd-trash───2*[{gvfsd-trash}]
        │         │       └─2*[{gvfsd}]
        │         ├─gvfsd-fuse───5*[{gvfsd-fuse}]
        │         ├─gvfsd-metadata───2*[{gvfsd-metadata}]
        │         ├─ibus-portal───2*[{ibus-portal}]
        │         ├─ibus-x11───2*[{ibus-x11}]
        │         ├─pipewire───{pipewire}
        │         ├─pipewire-media-───{pipewire-media-}
        │         ├─pulseaudio───3*[{pulseaudio}]
        │         ├─sh───ibus-daemon─┬─ibus-engine-lib───3*[{ibus-engine-lib}]
        │         │                  ├─ibus-extension-───3*[{ibus-extension-}]
        │         │                  ├─ibus-memconf───2*[{ibus-memconf}]
        │         │                  └─2*[{ibus-daemon}]
        │         ├─snap-store───6*[{snap-store}]
        │         ├─snapd-desktop-i───snapd-desktop-i───3*[{snapd-desktop-i}]
        │         ├─tracker-miner-f───5*[{tracker-miner-f}]
        │         ├─vmtoolsd───3*[{vmtoolsd}]
        │         ├─xdg-desktop-por───5*[{xdg-desktop-por}]
        │         ├─2*[xdg-desktop-por───3*[{xdg-desktop-por}]]
        │         ├─xdg-document-po─┬─fusermount3
        │         │                 └─5*[{xdg-document-po}]
        │         └─xdg-permission-───2*[{xdg-permission-}]
        ├─systemd-journal
        ├─systemd-logind
        ├─systemd-oomd
        ├─systemd-resolve
        ├─systemd-timesyn───{systemd-timesyn}
        ├─systemd-udevd
        ├─udisksd───4*[{udisksd}]
        ├─unattended-upgr───{unattended-upgr}
        ├─upowerd───2*[{upowerd}]
        ├─vmtoolsd───3*[{vmtoolsd}]
        ├─vmware-vmblock-───2*[{vmware-vmblock-}]
        └─wpa_supplicant
  • 操作系统只创建树根上面的进程,剩下所有的进程都是由树根上的进程创建的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ls /sbin/init -l
lrwxrwxrwx 1 root root 20 Sep 10  2022 /sbin/init -> /lib/systemd/systemd

┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ls /etc/init.d -l
total 88
-rwxr-xr-x 1 root root 3740 Feb 23  2022 apparmor
-rwxr-xr-x 1 root root 2915 May 10  2022 apport
-rwxr-xr-x 1 root root 1175 Dec 26  2021 binfmt-support
-rwxr-xr-x 1 root root 1232 Nov 23  2021 console-setup.sh
-rwxr-xr-x 1 root root 3062 Mar 18  2021 cron
-rwxr-xr-x 1 root root 3152 Jun 28  2021 dbus
-rwxr-xr-x 1 root root 1748 Feb 21  2022 hwclock.sh
-rwxr-xr-x 1 root root 2638 Oct 30  2021 irqbalance
-rwxr-xr-x 1 root root 1479 Jul 24  2021 keyboard-setup.sh
-rwxr-xr-x 1 root root 2044 Jan  8  2021 kmod
-rwxr-xr-x 1 root root 1386 Feb 23  2022 plymouth
-rwxr-xr-x 1 root root  760 Feb 23  2022 plymouth-log
-rwxr-xr-x 1 root root  959 Feb 25  2022 procps
-rwxr-xr-x 1 root root 4417 Oct 12  2022 rsync
-rwxr-xr-x 1 root root 1222 Feb 18  2021 screen-cleanup
-rwxr-xr-x 1 root root 6871 Mar  8  2022 udev
-rwxr-xr-x 1 root root 2083 Sep 19  2021 ufw
-rwxr-xr-x 1 root root 1391 Feb 19  2021 unattended-upgrades
-rwxr-xr-x 1 root root 1306 Feb 21  2022 uuidd
-rwxr-xr-x 1 root root 2762 Oct 19  2021 x11-common

┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ls /bin/sh -l
lrwxrwxrwx 1 root root 4 Feb 11 05:35 /bin/sh -> dash
  • 程序:状态机

    • C 代码视角:语句

    • 汇编/机器代码视角:指令

    • 与操作系统交互的方式:syscall

定制最小的 Linux

1
2
3
4
5
$ tree .
.
├── bin
│   └── busybox (可以在我们的Linux里直接执行)
└── init
  • 加上 vmlinuz (内核镜像) 就可以在 QEMU 里启动了

  • 可以直接在文件系统中添加静态链接的二进制文件

  • Makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.PHONY: initramfs run clean

$(shell mkdir -p build)

initramfs:
	@cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 \
	  > ../build/initramfs.cpio.gz

run:
	@qemu-system-x86_64 \
	  -nographic \
	  -serial mon:stdio \
	  -m 128 \
	  -kernel vmlinuz \
	  -initrd build/initramfs.cpio.gz \
	  -append "console=ttyS0 quiet acpi=off"

clean:
	@rm -rf build
  • 安装qemu
1
2
sudo apt-get install qemu
sudo apt-get install qemu-system
  • 运行
1
make run
  • 进入qemu $\rightarrow$ Ctrl a; c

  • 查看状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sh: can't access tty; job control turned off
/ # QEMU 6.2.0 monitor - type 'help' for more information
(qemu) info registers 
RAX=0001a94000000000 RBX=0000000000000000 RCX=0000000000000001 RDX=0000000000000ca2
RSI=0000000000000087 RDI=0000000000000087 RBP=ffffffffa3c03e28 RSP=ffffffffa3c03e08
R8 =ffff88a5c781df80 R9 =0000000000000200 R10=0000000000000000 R11=0000000000000000
R12=0000000000000000 R13=ffffffffa3c13780 R14=0000000000000000 R15=0000000000000000
RIP=ffffffffa30d564e RFL=00000246 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1
ES =0000 0000000000000000 00000000 00000000
CS =0010 0000000000000000 ffffffff 00af9b00 DPL=0 CS64 [-RA]
SS =0018 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0000 0000000000000000 00000000 00000000
FS =0000 0000000000000000 00000000 00000000
GS =0000 ffff88a5c7800000 00000000 00000000
LDT=0000 0000000000000000 00000000 00008200 DPL=0 LDT
TR =0040 fffffe0000003000 0000206f 00008900 DPL=0 TSS64-avl
GDT=     fffffe0000001000 0000007f
IDT=     fffffe0000000000 00000fff
CR0=80050033 CR2=0000000000e22c18 CR3=0000000003ca8000 CR4=000006f0
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
EFER=0000000000000d01
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=0000000000e20450 0000000000000400 XMM01=0000000000000000 0000000000000000
XMM02=0000000000000000 0000000000000000 XMM03=0000000000000000 0000000000000000
XMM04=ffffffffffffffff ffffffff00000000 XMM05=0000000048094038 3028201810080072
XMM06=0000000000ff0000 000000ff00000000 XMM07=0000000400000004 0000000400000004
XMM08=00000000008d93e0 00000000008d93e0 XMM09=0000000000000000 0000000000000000
XMM10=0000000000000000 0000000000000000 XMM11=0000000000000000 0000000000000000
XMM12=0000000000000000 0000000000000000 XMM13=0000000000000000 0000000000000000
XMM14=0000000000000000 0000000000000000 XMM15=0000000000000000 0000000000000000

变魔术时间到

1
2
3
4
5
6
7
8
9
c1="arch ash base64 cat chattr chgrp chmod chown conspy cp cpio cttyhack date dd df dmesg dnsdomainname dumpkmap echo ed egrep false fatattr fdflush fgrep fsync getopt grep gunzip gzip hostname hush ionice iostat ipcalc kbd_mode kill link linux32 linux64 ln login ls lsattr lzop makemime mkdir mknod mktemp more mount mountpoint mpstat mt mv netstat nice nuke pidof ping ping6 pipe_progress printenv ps pwd reformime resume rev rm rmdir rpm run-parts scriptreplay sed setarch setpriv setserial sh sleep stat stty su sync tar touch true umount uname usleep vi watch zcat"
c2="[ [[ awk basename bc beep blkdiscard bunzip2 bzcat bzip2 cal chpst chrt chvt cksum clear cmp comm crontab cryptpw cut dc deallocvt diff dirname dos2unix dpkg dpkg-deb du dumpleases eject env envdir envuidgid expand expr factor fallocate fgconsole find flock fold free ftpget ftpput fuser groups hd head hexdump hexedit hostid id install ipcrm ipcs killall last less logger logname lpq lpr lsof lspci lsscsi lsusb lzcat lzma man md5sum mesg microcom mkfifo mkpasswd nc nl nmeter nohup nproc nsenter nslookup od openvt passwd paste patch pgrep pkill pmap printf pscan"
c3="pstree pwdx readlink realpath renice reset resize rpm2cpio runsv runsvdir rx script seq setfattr setkeycodes setsid setuidgid sha1sum sha256sum sha3sum sha512sum showkey shred shuf smemcap softlimit sort split ssl_client strings sum sv svc svok tac tail taskset tcpsvd tee telnet test tftp time timeout top tr traceroute traceroute6 truncate ts tty ttysize udhcpc6 udpsvd unexpand uniq unix2dos unlink unlzma unshare unxz unzip uptime users uudecode uuencode vlock volname w wall wc wget which who whoami whois xargs xxd xz xzcat yes"
for cmd in $c1 $c2 $c3; do
  /bin/busybox ln -s /bin/busybox /bin/$cmd
done
mkdir -p /proc && mount -t proc  none /proc
mkdir -p /sys  && mount -t sysfs none /sys
export PS1='(linux) '
/img/Operating System/chapter11-1.png
创建链接,创建目录,挂载了一些内核的文件系统,将操作系统一些内部状态暴露给应用程序 -> 变成linux
/img/Operating System/chapter11-2.png
命令也可以用
  • 过程

例子:NOILinux-lite

  • 2021 年,CCF 以迅雷不及掩耳盗铃之势发布了 NOILinux 2.0

    • Ubuntu 20.04 Desktop (x86-64 only)

    • 真就不管那些 32-bit 的老爷机和老爷系统的死活了?

  • 和刚才的 “最小” 系统但本质一样

    • 有更多设备 (磁盘、网卡等)

    • initramfs 里挂载了磁盘

    • 磁盘里安装了最少的编译运行环境 (g++, ...) 和一个Web服务

    • switch_root (pivot_root 系统调用) 完成 “启动”

小结:应用程序视角的操作系统

  • Linux 操作系统启动流程
    • CPU ResetFirmwareLoader Kernel _start() → 第一个程序 /bin/init → 程序 (状态机) 执行 + 系统调用

  • 操作系统为 (所有) 程序提供 API

    • 进程 (状态机) 管理

      • fork, execve, exit - 状态机的创建/改变/删除 ← 今天的主题
    • 存储(内存) (地址空间) 管理

      • mmap - 虚拟地址空间管理
    • 文件 (数据对象) 管理

      • open, close, read, write - 文件访问管理
        • mkdir, link, unlink - 目录管理

fork()

操作系统:状态机的管理者

  • C 程序 = 状态机

    • 初始状态:main(argc, argv)

    • 程序可以直接在处理器上执行

  • 虚拟化:操作系统在物理内存中保存多个状态机(管理好多个状态机,每一次都可以选择一个状态机执行一步)

  • 通过虚拟内存实现每次 “拿出来一个执行”

  • 中断后进入操作系统代码,“换一个执行”

  • 除了fork的返回值pid,剩下的都一样,并且互相独立(第一个进程的指针访问不了第二个进程的内存)

  • 之后进入并发状态,执行那个由操作系统决定

状态机管理:创建状态机

如果要创建状态机,我们应该提供什么样的 API

  • UNIX 的答案: fork

    • 做一份状态机完整的复制 (内存、寄存器现场)
  • int fork();

    • 立即复制状态机 (完整的内存)

    • 新创建进程返回 0

    • 执行 fork 的进程返回子进程的进程号

Fork Bomb

  • 模拟状态机需要资源

    • 只要不停地创建进程,系统还是会挂掉的

    • Don't try it (or try it in docker)

代码解析: Fork Bomb

1
2
3
4
5
6
7
8
9
:(){ :|:&};:   # 刚才的一行版本

:() {         # 格式化一下
  : | : &
}; :

fork() {      # bash: 允许冒号作为标识符……
  fork | fork &
}; fork
  • 具体过程

这次你们记住 Fork 了!

  • 因为状态机是复制的,因此总能找到 “父子关系”
    • 因此有了进程树 (pstree)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
systemd-+-accounts-daemon---2*[{accounts-daemon}]
        |-agetty
        |-atd
        |-automount---2*[{automount}]
        |-avahi-daemon---avahi-daemon
        |-cron
        |-dbus-daemon
        |-irqbalance---{irqbalance}
        |-lxcfs---7*[{lxcfs}]
        ...

理解 fork: 习题 (1)

  • 试着拿出一张纸,写出以下程序的输出结果
1
2
3
4
5
6
7
8
9
#include <unistd.h>
#include <stdio.h>

int main() {
  pid_t pid1 = fork();
  pid_t pid2 = fork();
  pid_t pid3 = fork();
  printf("Hello World from (%d, %d, %d)\n", pid1, pid2, pid3);
}
  • 参考原来的OS作业

Including the initial parent process, how many processes are created by the program shown in Figure 3.31?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <unistd.h>

int main() {
    /* fork a child process */
    fork();
    /* fork another child process */
    fork();
    /* and fork another */
    fork();
    return 0;
}

$$ 1+3+3+1=8 $$

理解 fork: 习题 (2)

问以下程序的输出结果

  • 一个更好的版本: fork-printf.c
    • 用状态机的视角再试一次
    • 试一试:./a.out v.s. ./a.out | cat
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int n = 2;
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello\n");
  }
  for (int i = 0; i < n; i++) {
    wait(NULL);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf        
Hello
Hello
Hello
Hello
Hello
Hello
                                                                                                                                                              
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf | wc -l
8
  • why ?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf | cat
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
  • ???
  • 计算机系统里没有魔法。机器永远是对的。
  • 先看一个例子demo.c
1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    printf("Hello");
    int *p;
    p = NULL;
    *p = 1;
}
1
2
3
4
5
6
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ gcc demo.c -o demo   
                                                                                                                                                              
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./demo
zsh: segmentation fault  ./demo
  • 没有输出Hello

  • 改一下

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    printf("Hello\n");
    int *p;
    p = NULL;
    *p = 1;
}
1
2
3
4
5
6
7
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ gcc demo.c -o demo
                                                                                                                                                              
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./demo            
Hello
zsh: segmentation fault  ./demo
  • 联系之前的学校的实验

main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
  printf("a");
  fflush(stdout);
  pid_t pid = fork();
  if (pid < 0) {
    printf("Error!\n");
  } else if (pid == 0) {
    execl("child", "", NULL);
    exit(0);
  } else {
    wait(NULL);
    printf("c");
  }
  return 0;
}

child.c

1
2
3
4
5
6
7
#include <stdio.h>
#include <unistd.h>

int main() {
  printf("b");
  return 0;
}
  • 缓冲区问题

在调用fork()前先printf("a"),在fork()后的子进程里printf("b"),这样结果按理来说是打印"ab",但是运行出来反而是"ba"。 这个问题的原因是c语言的输出缓冲,在调用printf()函数时,数据先被存放在缓冲区中,待缓冲区满了或者遇到了换行符"\n"时才会输出到屏幕上。printf("a")语句输出字符"a",但由于缓冲区未满,字符"a"并没有被立即输出。当程序调用fork()函数创建子进程时,子进程也继承了父进程的缓冲区,这时缓冲区中的数据包括字符"a"也被复制到了子进程的缓冲区中。接着,在子进程中,printf("b")语句输出字符"b",由于子进程的缓冲区已经满了,因此字符"a"和字符"b"都被输出到屏幕上,此时的输出结果是"ba"

解决的方法是在printf("a")语句后加上fflush(stdout)语句,强制将缓冲区的数据输出到屏幕上,这样输出的结果就是"ab"

  • linebuffer:遇到\n通过系统调用将缓冲区的内容输出

  • fullbuffer:到达设定容量后才会将缓冲区的内容输出


  • 下面来分析
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf | cat
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
/img/Operating System/chapter11-3.jpg
树状图
  • 另一种解决方案:setbuf(stdout,NULL);强行将标准输出流设置为不缓冲
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  setbuf(stdout, NULL);
  int n = 2;
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello\n");
  }
  for (int i = 0; i < n; i++) {
    wait(NULL);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf | cat             
Hello
Hello
Hello
Hello
Hello
Hello
                                                                                                                                                   
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./fork-printf | wc -l
6

理解 fork: 习题 (3)

  • 多线程程序的某个线程执行 fork(),应该发生什么?

    • 这是个很有趣的问题:创造 fork 时创始人并没有考虑线程
  • 我们可能作出以下设计:

    • 仅有执行 fork 的线程被复制,其他线程 “卡死”

    • 仅有执行 fork 的线程被复制,其他线程退出

    • 所有的线程都被复制并继续执行

      • 这三种设计分别会带来什么问题?

execve()

状态机管理:替换状态机

光有 fork 还不够,怎么 “执行别的程序”?

  • UNIX 的答案: execve
    • 将当前运行的状态机重置成成另一个程序的初始状态
1
int execve(const char *filename, char * const argv, char * const envp);
  • 执行名为 filename 的程序

  • 允许对新状态机设置参数 argv (v) 和环境变量 envp (e),数组形式,以空指针NULL来结尾

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <unistd.h>
#include <stdio.h>

int main() {
  char * const argv[] = {
    "/bin/bash", "-c", "env", NULL,
  };
  char * const envp[] = {
    "HELLO=WORLD", NULL,
  };
  execve(argv[0], argv, envp);
  printf("Hello, World!\n");
}
1
2
3
4
5
6
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./execve-demo
PWD=/mnt/d/work for vscode/chapter11
HELLO=WORLD
SHLVL=0
_=/usr/bin/env
  • printf没有被执行(原来的状态机没有了)
  • 参照
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ bash -c ls
demo  demo.c  execve-demo  execve-demo.c  fork-demo  fork-demo.c  fork-printf  fork-printf.c  l.zip  linux-minimal  thread-os.c
                                                                                                                                                   
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ bash -c env
SHELL=/usr/bin/zsh
USER_ZDOTDIR=/home/jungle
COLORTERM=truecolor
WSL2_GUI_APPS_ENABLED=1
TERM_PROGRAM_VERSION=1.78.0
WSL_DISTRO_NAME=Ubuntu-22.04
LESS_TERMCAP_se=
LESS_TERMCAP_so=
NAME=LAPTOP-A7S3TAA4
PWD=/mnt/d/work for vscode/chapter11
LOGNAME=jungle
_=/usr/bin/env
...
...
...

环境变量

“应用程序执行的环境”

  • 使用env命令查看

    • PATH: 可执行文件搜索路径
    • PWD: 当前路径
    • HOME: home 目录
    • DISPLAY: 图形输出
    • PS1: shell 的提示符
  • export

    告诉 shell 在创建子进程时设置环境变量

    • 小技巧:export ARCH=x86_64-qemuexport ARCH=native
    • 上学期的 AM_HOME 终于破案了(这是南大的,邮专上学期还在写数据结构)
1
2
3
4
5
6
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ export HELLO=WORLD
                                                                                                                                                   
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ env | grep HELLO
HELLO=WORLD
/img/Operating System/chapter11-4.png
第一个调用总是execve()
1
2
3
4
5
6
7
8
9
...
mprotect(0x7f373bcba000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7f373bc74000, 46367)           = 0
execve("/bin/bash", ["/bin/bash", "-c", "env"], 0x7fffa5c787c0 /* 1 var */) = 0
brk(NULL)                               = 0x559dfe4ca000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe8ef996f0) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f476f6f0000
...
  • 最终输出的syscall
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
write(1, "PWD=/mnt/d/work for vscode/chapt"..., 37PWD=/mnt/d/work for vscode/chapter11
) = 37
write(1, "HELLO=WORLD\n", 12HELLO=WORLD
)           = 12
write(1, "SHLVL=0\n", 8SHLVL=0
)                = 8
write(1, "_=/usr/bin/env\n", 15_=/usr/bin/env
)        = 15
...

环境变量:PATH

  • 可执行文件搜索路径
    • 还记得 gccstrace 结果吗?
1
2
3
4
[pid 28369] execve("/usr/local/sbin/as", ["as", "--64", ...
[pid 28369] execve("/usr/local/bin/as", ["as", "--64", ...
[pid 28369] execve("/usr/sbin/as", ["as", "--64", ...
[pid 28369] execve("/usr/bin/as", ["as", "--64", ...
  • 这个搜索顺序恰好是 PATH 里指定的顺序
1
2
3
$ PATH="" /usr/bin/gcc a.c
gcc: error trying to exec 'as': execvp: No such file or directory
$ PATH="/usr/bin/" gcc a.c
  • fork是状态机的复制,execve是状态机的重置,环境变量就是重置状态机的参数

_exit()

状态机管理:终止状态机

有了 fork, execve 我们就能自由执行任何程序了,最后只缺一个销毁状态机的函数!

  • UNIX 的答案: _exit
    • 立即摧毁状态机
1
void _exit(int status);
  • 销毁当前状态机,并允许有一个返回值

  • 子进程终止会通知父进程 (后续课程解释)

  • 这个简单……

    • 但问题来了:多线程程序怎么办?

结束程序执行的三种方法

  • exit 的几种写法 (它们是不同)

    • exit(0) $\rightarrow$ stdlib.h 中声明的libc函数

      • 会调用 atexit
    • _exit(0) - glibc 的 syscall wrapper

      • 执行 “exit_group” 系统调用终止整个进程 (所有线程)
        • 细心的同学已经在 strace 中发现了
      • 不会调用 atexit
    • syscall(SYS_exit, 0)

      • 执行 “exit” 系统调用终止当前线程
      • 不会调用 atexit

不妨试一试

  • 结束当前进程执行的四种方式
    • return, exit, _exit, syscall
    • exit-demo.c
      • strace观察程序的执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/syscall.h>

void func() {
  printf("Goodbye, Cruel OS World!\n");
}

int main(int argc, char *argv[]) {
  atexit(func);

  if (argc < 2) return EXIT_FAILURE;

  if (strcmp(argv[1], "exit") == 0)
    exit(0);
  if (strcmp(argv[1], "_exit") == 0)
    _exit(0);
  if (strcmp(argv[1], "__exit") == 0)
    syscall(SYS_exit, 0);
}
1
2
3
4
5
6
7
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./exit-demo
Goodbye, Cruel OS World!
                                                                                                                                                              
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./exit-demo exit
Goodbye, Cruel OS World!
  • 使用static编译,省去链接过程,便于查看strace
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ strace ./exit-demo
execve("./exit-demo", ["./exit-demo"], 0x7ffdaa2866a0 /* 42 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd390434e0) = -1 EINVAL (Invalid argument)
brk(NULL)                               = 0x9b6000
brk(0x9b6dc0)                           = 0x9b6dc0
arch_prctl(ARCH_SET_FS, 0x9b63c0)       = 0
set_tid_address(0x9b6690)               = 8090
set_robust_list(0x9b66a0, 24)           = 0
rseq(0x9b6d60, 0x20, 0, 0x53053053)     = 0
uname({sysname="Linux", nodename="LAPTOP-A7S3TAA4", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/mnt/d/work for vscode/chapter11"..., 4096) = 42
getrandom("\xfc\x1b\x9a\x3b\x0c\x0d\xf6\x62", 8, GRND_NONBLOCK) = 8
brk(0x9d7dc0)                           = 0x9d7dc0
brk(0x9d8000)                           = 0x9d8000
mprotect(0x4c1000, 16384, PROT_READ)    = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x7), ...}, AT_EMPTY_PATH) = 0
write(1, "Goodbye, Cruel OS World!\n", 25Goodbye, Cruel OS World!
) = 25
exit_group(1)                           = ?
+++ exited with 1 +++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ strace ./exit-demo _exit
execve("./exit-demo", ["./exit-demo", "_exit"], 0x7ffdcbeb2108 /* 42 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff2fcb4900) = -1 EINVAL (Invalid argument)
brk(NULL)                               = 0x1346000
brk(0x1346dc0)                          = 0x1346dc0
arch_prctl(ARCH_SET_FS, 0x13463c0)      = 0
set_tid_address(0x1346690)              = 8124
set_robust_list(0x13466a0, 24)          = 0
rseq(0x1346d60, 0x20, 0, 0x53053053)    = 0
uname({sysname="Linux", nodename="LAPTOP-A7S3TAA4", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/mnt/d/work for vscode/chapter11"..., 4096) = 42
getrandom("\x1f\x9d\x12\x25\xe1\x65\xc9\x7b", 8, GRND_NONBLOCK) = 8
brk(0x1367dc0)                          = 0x1367dc0
brk(0x1368000)                          = 0x1368000
mprotect(0x4c1000, 16384, PROT_READ)    = 0
exit_group(0)                           = ?
+++ exited with 0 +++
1
2
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ ./exit-demo _exit   
  • 没有Goodbye
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
┌──(jungle㉿LAPTOP-A7S3TAA4)-[/mnt/d/work for vscode/chapter11]
└─$ strace ./exit-demo __exit
execve("./exit-demo", ["./exit-demo", "__exit"], 0x7ffeca752f48 /* 42 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe37ad6940) = -1 EINVAL (Invalid argument)
brk(NULL)                               = 0x15df000
brk(0x15dfdc0)                          = 0x15dfdc0
arch_prctl(ARCH_SET_FS, 0x15df3c0)      = 0
set_tid_address(0x15df690)              = 8256
set_robust_list(0x15df6a0, 24)          = 0
rseq(0x15dfd60, 0x20, 0, 0x53053053)    = 0
uname({sysname="Linux", nodename="LAPTOP-A7S3TAA4", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/mnt/d/work for vscode/chapter11"..., 4096) = 42
getrandom("\xed\x64\xb4\xdb\x93\xb2\x77\xd0", 8, GRND_NONBLOCK) = 8
brk(0x1600dc0)                          = 0x1600dc0
brk(0x1601000)                          = 0x1601000
mprotect(0x4c1000, 16384, PROT_READ)    = 0
exit(0)                                 = ?
+++ exited with 0 +++
  • exit只关闭一个线程,_exit $\rightarrow^{Syscall}$ exit_group会把整个所有的线程都删掉
  • linux默认用的_exit,安全

总结

  • 本次课回答的问题

    • Q1: 操作系统启动后到底做了什么?

    • Q2: 操作系统如何管理程序 (进程)?

  • Take-away messages

    • 对 “操作系统” 的完整理解

    • CPU Reset → Firmware → Loader → Kernel _start() → 执行第一个程序 /bin/init → 中断/异常处理程序

    • 一个最小的 Linux 系统的例子

  • 进程管理 API

    • fork, execve, exit: 状态机的复制、重置、销毁
    • 理论上就可以实现 “各种功能” 了!

声明:本文章引用资料与图像均已做标注,如有侵权本人会马上删除