Linux 进程必知必会

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇文章只是简单的描述了一下 Linux 基本概念,通过几个例子来说明 Linux 基本应用程序,然后以 Linux 基本内核构造来结尾。那么本篇文章我们就深入理解一下 Linux 内核来理解 Linux 的基本概念之进程和线程。系统调用是操作系统本身的接口,它对于创建进程和线程,内存分配,共享文件和 I/O 来说都很重要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们将从各个版本的共性出发来进行探讨。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基本概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 一个非常重要的概念就是进程,Linux 进程和我们在"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"进程和线程 这篇文章"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"中探讨的进程模型非常相似。每个进程都会运行一段独立的程序,并且在初始化的时候拥有一个独立的控制线程。换句话说,每个进程都会有一个自己的程序计数器,这个程序计数器用来记录下一个需要被执行的指令。Linux 允许进程在运行时创建额外的线程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c5/c587203854d07078acbbd913dd002b27.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 是一个多道程序设计系统,因此系统中存在彼此相互独立的进程同时运行。此外,每个用户都会同时有几个活动的进程。因为如果是一个大型系统,可能有数百上千的进程在同时运行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些用户空间中,即使用户退出登录,仍然会有一些后台进程在运行,这些进程被称为 "},{"type":"codeinline","content":[{"type":"text","text":"守护进程(daemon)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 中有一种特殊的守护进程被称为 "},{"type":"codeinline","content":[{"type":"text","text":"计划守护进程(Cron daemon)"}]},{"type":"text","text":" ,计划守护进程可以每分钟醒来一次检查是否有工作要做,做完会继续回到睡眠状态等待下一次唤醒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/70/7015a7b4456c2edd37fb006457b80182.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Cron 是一个守护程序,可以做任何你想做的事情,比如说你可以定期进行系统维护、定期进行系统备份等。在其他操作系统上也有类似的程序,比如 Mac OS X 上 Cron 守护程序被称为 "},{"type":"codeinline","content":[{"type":"text","text":"launchd"}]},{"type":"text","text":" 的守护进程。在 Windows 上可以被称为 "},{"type":"codeinline","content":[{"type":"text","text":"计划任务(Task Scheduler)"}]},{"type":"text","text":"。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Linux 系统中,进程通过非常简单的方式来创建,"},{"type":"codeinline","content":[{"type":"text","text":"fork"}]},{"type":"text","text":" 系统调用会创建一个源进程的"},{"type":"codeinline","content":[{"type":"text","text":"拷贝(副本)"}]},{"type":"text","text":"。调用 fork 函数的进程被称为 "},{"type":"codeinline","content":[{"type":"text","text":"父进程(parent process)"}]},{"type":"text","text":",使用 fork 函数创建出来的进程被称为 "},{"type":"codeinline","content":[{"type":"text","text":"子进程(child process)"}]},{"type":"text","text":"。父进程和子进程都有自己的内存映像。如果在子进程创建出来后,父进程修改了一些变量等,那么子进程是看不到这些变化的,也就是 fork 后,父进程和子进程相互独立。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虽然父进程和子进程保持相互独立,但是它们却能够共享相同的文件,如果在 fork 之前,父进程已经打开了某个文件,那么 fork 后,父进程和子进程仍然共享这个打开的文件。对共享文件的修改会对父进程和子进程同时可见。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么该如何区分父进程和子进程呢?子进程只是父进程的拷贝,所以它们几乎所有的情况都一样,包括内存映像、变量、寄存器等。区分的关键在于 "},{"type":"codeinline","content":[{"type":"text","text":"fork "}]},{"type":"text","text":" 函数调用后的返回值,如果 fork 后返回一个非零值,这个非零值即是子进程的 "},{"type":"codeinline","content":[{"type":"text","text":"进程标识符(Process Identiier, PID)"}]},{"type":"text","text":",而会给子进程返回一个零值,可以用下面代码来进行表示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"pid = fork(); // 调用 fork 函数创建进程\nif(pid < 0){\n error()\t\t\t\t // pid < 0,创建失败\n}\nelse if(pid > 0){\n parent_handle() // 父进程代码\n}\nelse {\n child_handle() // 子进程代码\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"父进程在 fork 后会得到子进程的 PID,这个 PID 即能代表这个子进程的唯一标识符也就是 PID。如果子进程想要知道自己的 PID,可以调用 "},{"type":"codeinline","content":[{"type":"text","text":"getpid"}]},{"type":"text","text":" 方法。当子进程结束运行时,父进程会得到子进程的 PID,因为一个进程会 fork 很多子进程,子进程也会 fork 子进程,所以 PID 是非常重要的。我们把第一次调用 fork 后的进程称为 "},{"type":"codeinline","content":[{"type":"text","text":"原始进程"}]},{"type":"text","text":",一个原始进程可以生成一颗继承树"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5b/5b16b85030688e14042066294bc7a3a6.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Linux 进程间通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 进程间的通信机制通常被称为 "},{"type":"codeinline","content":[{"type":"text","text":"Internel-Process communication,IPC "}]},{"type":"text","text":"下面我们来说一说 Linux 进程间通信的机制,大致来说,Linux 进程间的通信机制可以分为 6 种"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a8/a850c700d19e0f7410c30e8aa262868d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我们分别对其进行概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"信号 signal"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送"},{"type":"codeinline","content":[{"type":"text","text":"异步事件信号"}]},{"type":"text","text":"来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以在 Linux 系统上输入 "},{"type":"codeinline","content":[{"type":"text","text":"kill -l"}]},{"type":"text","text":" 来列出系统使用的信号,下面是我提供的一些信号"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ecabc6cdc00b70328472eecd089549b9.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:"},{"type":"codeinline","content":[{"type":"text","text":"SIGSTOP"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"SIGKILL"}]},{"type":"text","text":" 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如:当进程收到 "},{"type":"codeinline","content":[{"type":"text","text":"SIGFPE"}]},{"type":"text","text":" 浮点异常的信号后,默认操作是对其进行 "},{"type":"codeinline","content":[{"type":"text","text":"dump(转储)"}]},{"type":"text","text":"和退出。信号没有优先级的说法。如果同时为某个进程产生了两个信号,则可以将它们呈现给进程或者以任意的顺序进行处理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我们就来看一下这些信号是干什么用的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGABRT 和 SIGIOT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGABRT 和 SIGIOT 信号发送给进程,告诉其进行终止,这个 信号通常在调用 C标准库的"},{"type":"codeinline","content":[{"type":"text","text":"abort()"}]},{"type":"text","text":"函数时由进程本身启动"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGALRM 、 SIGVTALRM、SIGPROF"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当设置的时钟功能超时时会将 SIGALRM 、 SIGVTALRM、SIGPROF 发送给进程。当实际时间或时钟时间超时时,发送 SIGALRM。 当进程使用的 CPU 时间超时时,将发送 SIGVTALRM。 当进程和系统代表进程使用的CPU 时间超时时,将发送 SIGPROF。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGBUS"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGBUS 将造成"},{"type":"codeinline","content":[{"type":"text","text":"总线中断"}]},{"type":"text","text":"错误时发送给进程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCHLD"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当子进程终止、被中断或者被中断恢复,将 SIGCHLD 发送给进程。此信号的一种常见用法是指示操作系统在子进程终止后清除其使用的资源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCONT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCONT 信号指示操作系统继续执行先前由 SIGSTOP 或 SIGTSTP 信号暂停的进程。该信号的一个重要用途是在 Unix shell 中的作业控制中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGFPE"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGFPE 信号在执行错误的算术运算(例如除以零)时将被发送到进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/34/34d72231d13d0b06708c43e4a7de0400.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGUP 信号控制的终端关闭时,会发送给进程。许多守护程序将重新加载其配置文件并重新打开其日志文件,而不是在收到此信号时退出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGILL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGILL 信号在尝试执行非法、格式错误、未知或者特权指令时发出"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGINT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当用户希望中断进程时,操作系统会向进程发送 SIGINT 信号。用户输入 ctrl - c 就是希望中断进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL 信号发送到进程以使其马上进行终止。 与 SIGTERM 和 SIGINT 相比,这个信号无法捕获和忽略执行,并且进程在接收到此信号后无法执行任何清理操作,下面是一些例外情况"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"僵尸进程无法杀死,因为僵尸进程已经死了,它在等待父进程对其进行捕获"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"处于阻塞状态的进程只有再次唤醒后才会被 kill 掉"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"init"}]},{"type":"text","text":" 进程是 Linux 的初始化进程,这个进程会忽略任何信号。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL 通常是作为最后杀死进程的信号、它通常作用于 SIGTERM 没有响应时发送给进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPIPE"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPIPE 尝试写入进程管道时发现管道未连接无法写入时发送到进程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPOLL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当在明确监视的文件描述符上发生事件时,将发送 SIGPOLL 信号。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGRTMIN 至 SIGRTMAX"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGRTMIN 至 SIGRTMAX 是"},{"type":"codeinline","content":[{"type":"text","text":"实时信号"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGQUIT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当用户请求退出进程并执行核心转储时,SIGQUIT 信号将由其控制终端发送给进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSEGV"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGSEGV 信号做出无效的虚拟内存引用或分段错误时,即在执行分段违规时,将其发送到进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSTOP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSTOP 指示操作系统终止以便以后进行恢复时"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSYS"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGSYS 信号将错误参数传递给系统调用时,该信号将发送到进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SYSTERM"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们上面简单提到过了 SYSTERM 这个名词,这个信号发送给进程以请求终止。与 SIGKILL 信号不同,该信号可以被过程捕获或忽略。这允许进程执行良好的终止,从而释放资源并在适当时保存状态。 SIGINT 与SIGTERM 几乎相同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTSIP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTSTP 信号由其控制终端发送到进程,以请求终端停止。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTTIN 和 SIGTTOU"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGTTIN 和SIGTTOU 信号分别在后台尝试从 tty 读取或写入时,信号将发送到该进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" SIGTRAP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在发生异常或者 trap 时,将 SIGTRAP 信号发送到进程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGURG"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当套接字具有可读取的紧急或带外数据时,将 SIGURG 信号发送到进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUSR1 和 SIGUSR2"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUSR1 和 SIGUSR2 信号被发送到进程以指示用户定义的条件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGXCPU"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGXCPU 信号耗尽 CPU 的时间超过某个用户可设置的预定值时,将其发送到进程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGXFSZ"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当 SIGXFSZ 信号增长超过最大允许大小的文件时,该信号将发送到该进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGWINCH"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGWINCH 信号在其控制终端更改其大小(窗口更改)时发送给进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"管道 pipe"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系统中的进程可以通过建立管道 pipe 进行通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/95/95c00f29b100cdcd810ee6b8d0bb5723.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的"},{"type":"codeinline","content":[{"type":"text","text":"管线 pipelines"}]},{"type":"text","text":" 就是用管道实现的,当 shell 发现输出 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"sort "},{"type":"codeinline","content":[{"type":"text","text":"进程位于内存"}]},{"type":"text","text":"被称为 "},{"type":"codeinline","content":[{"type":"text","text":"PIM(Process In Memory)"}]},{"type":"text","text":" ,这是冯诺伊曼体系架构的一种体现,加载到内存中并执行的程序称为进程。简单来说,一个进程就是正在执行的程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"进程描述符可以归为下面这几类"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"调度参数(scheduling parameters)"}]},{"type":"text","text":":进程优先级、最近消耗 CPU 的时间、最近睡眠时间一起决定了下一个需要运行的进程"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"内存映像(memory image)"}]},{"type":"text","text":":我们上面说到,进程映像是执行程序时所需要的可执行文件,它由数据和代码组成。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"信号(signals)"}]},{"type":"text","text":":显示哪些信号被捕获、哪些信号被执行"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"寄存器"}]},{"type":"text","text":":当发生内核陷入 (trap) 时,寄存器的内容会被保存下来。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"系统调用状态(system call state)"}]},{"type":"text","text":":当前系统调用的信息,包括参数和结果"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"文件描述符表(file descriptor table)"}]},{"type":"text","text":":有关文件描述符的系统被调用时,文件描述符作为索引在文件描述符表中定位相关文件的 i-node 数据结构"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"统计数据(accounting)"}]},{"type":"text","text":":记录用户、进程占用系统 CPU 时间表的指针,一些操作系统还保存进程最多占用的 CPU 时间、进程拥有的最大堆栈空间、进程可以消耗的页面数等。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"内核堆栈(kernel stack)"}]},{"type":"text","text":":进程的内核部分可以使用的固定堆栈"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"其他"}]},{"type":"text","text":": 当前进程状态、事件等待时间、距离警报的超时时间、PID、父进程的 PID 以及用户标识符等"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了上面这些信息,现在就很容易描述在 Linux 中是如何创建这些进程的了,创建新流程实际上非常简单。"},{"type":"text","marks":[{"type":"strong"}],"text":"为子进程开辟一块新的用户空间的进程描述符,然后从父进程复制大量的内容。为这个子进程分配一个 PID,设置其内存映射,赋予它访问父进程文件的权限,注册并启动"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当执行 fork 系统调用时,调用进程会陷入内核并创建一些和任务相关的数据结构,比如"},{"type":"codeinline","content":[{"type":"text","text":"内核堆栈(kernel stack)"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"thread_info"}]},{"type":"text","text":" 结构。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">关于 thread_info 结构可以参考 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch"},{"type":"text","marks":[{"type":"italic"}],"text":"2avr32"},{"type":"text","text":"2include"},{"type":"text","marks":[{"type":"italic"}],"text":"2asm"},{"type":"text","text":"2thread_"},{"type":"text","marks":[{"type":"italic"}],"text":"info"},{"type":"text","text":"8h_source.html"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个结构中包含进程描述符,进程描述符位于固定的位置,使得 Linux 系统只需要很小的开销就可以定位到一个运行中进程的数据结构。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"进程描述符的主要内容是根据"},{"type":"codeinline","content":[{"type":"text","text":"父进程"}]},{"type":"text","text":"的描述符来填充。Linux 操作系统会寻找一个可用的 PID,并且此 PID 没有被任何进程使用,更新进程标示符使其指向一个新的数据结构即可。为了减少 hash table 的碰撞,进程描述符会形成"},{"type":"codeinline","content":[{"type":"text","text":"链表"}]},{"type":"text","text":"。它还将 task_struct 的字段设置为指向任务数组上相应的上一个/下一个进程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">task_struct : Linux 进程描述符,内部涉及到众多 C++ 源码,我们会在后面进行讲解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从原则上来说,为子进程开辟内存区域并为子进程分配数据段、堆栈段,并且对父进程的内容进行复制,但是实际上 fork 完成后,子进程和父进程没有共享内存,所以需要复制技术来实现同步,但是复制开销比较大,因此 Linux 操作系统使用了一种 "},{"type":"codeinline","content":[{"type":"text","text":"欺骗"}]},{"type":"text","text":" 方式。即为子进程分配页表,然后新分配的页表指向父进程的页面,同时这些页面是只读的。当进程向这些页面进行写入的时候,会开启保护错误。内核发现写入操作后,会为进程分配一个副本,使得写入时把数据复制到这个副本上,这个副本是共享的,这种方式称为 "},{"type":"codeinline","content":[{"type":"text","text":"写入时复制(copy on write)"}]},{"type":"text","text":",这种方式避免了在同一块内存区域维护两个副本的必要,节省内存空间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在子进程开始运行后,操作系统会调用 exec 系统调用,内核会进行查找验证可执行文件,把参数和环境变量复制到内核,释放旧的地址空间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在新的地址空间需要被创建和填充。如果系统支持映射文件,就像 Unix 系统一样,那么新的页表就会创建,表明内存中没有任何页,除非所使用的页面是堆栈页,其地址空间由磁盘上的可执行文件支持。新进程开始运行时,立刻会收到一个"},{"type":"codeinline","content":[{"type":"text","text":"缺页异常(page fault)"}]},{"type":"text","text":",这会使具有代码的页面加载进入内存。最后,参数和环境变量被复制到新的堆栈中,重置信号,寄存器全部清零。新的命令开始运行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是一个示例,用户输出 ls,shell 会调用 fork 函数复制一个新进程,shell 进程会调用 exec 函数用可执行文件 ls 的内容覆盖它的内存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aafb3f23ded5355a4a5a91c88c85c874.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 线程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在我们来讨论一下 Linux 中的线程,线程是轻量级的进程,想必这句话你已经听过很多次了,"},{"type":"codeinline","content":[{"type":"text","text":"轻量级"}]},{"type":"text","text":"体现在所有的进程切换都需要清除所有的表、进程间的共享信息也比较麻烦,一般来说通过管道或者共享内存,如果是 fork 函数后的父子进程则使用共享文件,然而线程切换不需要像进程一样具有昂贵的开销,而且线程通信起来也更方便。线程分为两种:用户级线程和内核级线程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"用户级线程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用户级线程避免使用内核,通常,每个线程会显示调用开关,发送信号或者执行某种切换操作来放弃 CPU,同样,计时器可以强制进行开关,用户线程的切换速度通常比内核线程快很多。在用户级别实现线程会有一个问题,即单个线程可能会垄断 CPU 时间片,导致其他线程无法执行从而 "},{"type":"codeinline","content":[{"type":"text","text":"饿死"}]},{"type":"text","text":"。如果执行一个 I/O 操作,那么 I/O 会阻塞,其他线程也无法运行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c1f0d88726f1343a241423991896b36b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一种解决方案是,一些用户级的线程包解决了这个问题。可以使用时钟周期的监视器来控制第一时间时间片独占。然后,一些库通过特殊的包装来解决系统调用的 I/O 阻塞问题,或者可以为非阻塞 I/O 编写任务。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"内核级线程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内核级线程通常使用几个进程表在内核中实现,每个任务都会对应一个进程表。在这种情况下,内核会在每个进程的时间片内调度每个线程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e82c968cc32998fb925efe9f4813b97a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从用户空间 -> 内核空间 -> 用户空间的开销比较大,但是线程初始化的时间损耗可以忽略不计。这种实现的好处是由时钟决定线程切换时间,因此不太可能将时间片与任务中的其他线程占用时间绑定到一起。同样,I/O 阻塞也不是问题。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"混合实现"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"结合用户空间和内核空间的优点,设计人员采用了一种"},{"type":"codeinline","content":[{"type":"text","text":"内核级线程"}]},{"type":"text","text":"的方式,然后将用户级线程与某些或者全部内核线程多路复用起来"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5a/5aeff42dcfdfa3d0eb3a38d3288863cb.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 调度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我们来关注一下 Linux 系统的调度算法,首先需要认识到,Linux 系统的线程是内核线程,所以 Linux 系统是基于线程的,而不是基于进程的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了进行调度,Linux 系统将线程分为三类"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实时先入先出"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实时轮询"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分时"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实时先入先出线程具有最高优先级,它不会被其他线程所抢占,除非那是一个刚刚准备好的,拥有更高优先级的线程进入。实时轮转线程与实时先入先出线程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占。如果多个实时线程准备完毕,那么每个线程运行它时间量所规定的时间,然后插入到实时轮转线程末尾。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">注意这个实时只是相对的,无法做到绝对的实时,因为线程的运行时间无法确定。它们相对分时系统来说,更加具有实时性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系统会给每个线程分配一个 "},{"type":"codeinline","content":[{"type":"text","text":"nice"}]},{"type":"text","text":" 值,这个值代表了优先级的概念。nice 值默认值是 0 ,但是可以通过系统调用 nice 值来修改。修改值的范围从 -20 - +19。nice 值决定了线程的静态优先级。一般系统管理员的 nice 值会比一般线程的优先级高,它的范围是 -20 - -1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我们更详细的讨论一下 Linux 系统的两个调度算法,它们的内部与"},{"type":"codeinline","content":[{"type":"text","text":"调度队列(runqueue)"}]},{"type":"text","text":" 的设计很相似。运行队列有一个数据结构用来监视系统中所有可运行的任务并选择下一个可以运行的任务。每个运行队列和系统中的每个 CPU 有关。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Linux O(1)"}]},{"type":"text","text":" 调度器是历史上很流行的一个调度器。这个名字的由来是因为它能够在常数时间内执行任务调度。在 O(1) 调度器里,调度队列被组织成两个数组,一个是任务"},{"type":"text","marks":[{"type":"strong"}],"text":"正在活动"},{"type":"text","text":"的数组,一个是任务"},{"type":"text","marks":[{"type":"strong"}],"text":"过期失效"},{"type":"text","text":"的数组。如下图所示,每个数组都包含了 140 个链表头,每个链表头具有不同的优先级。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6dc4f90e5f299cb8dcbf852ea3e32729.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调度器从正在活动数组中选择一个优先级最高的任务。如果这个任务的时间片过期失效了,就把它移动到过期失效数组中。如果这个任务阻塞了,比如说正在等待 I/O 事件,那么在它的时间片过期失效之前,一旦 I/O 操作完成,那么这个任务将会继续运行,它将被放回到之前正在活动的数组中,因为这个任务之前已经消耗一部分 CPU 时间片,所以它将运行剩下的时间片。当这个任务运行完它的时间片后,它就会被放到过期失效数组中。一旦正在活动的任务数组中没有其他任务后,调度器将会交换指针,使得正在活动的数组变为过期失效数组,过期失效数组变为正在活动的数组。使用这种方式可以保证每个优先级的任务都能够得到执行,不会导致线程饥饿。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在这种调度方式中,不同优先级的任务所得到 CPU 分配的时间片也是不同的,高优先级进程往往能得到较长的时间片,低优先级的任务得到较少的时间片。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这种方式为了保证能够更好的提供服务,通常会为 "},{"type":"codeinline","content":[{"type":"text","text":"交互式进程"}]},{"type":"text","text":" 赋予较高的优先级,交互式进程就是"},{"type":"codeinline","content":[{"type":"text","text":"用户进程"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系统不知道一个任务究竟是 I/O 密集型的还是 CPU 密集型的,它只是依赖于交互式的方式,Linux 系统会区分是"},{"type":"codeinline","content":[{"type":"text","text":"静态优先级"}]},{"type":"text","text":" 还是 "},{"type":"codeinline","content":[{"type":"text","text":"动态优先级"}]},{"type":"text","text":"。动态优先级是采用一种奖励机制来实现的。奖励机制有两种方式:"},{"type":"text","marks":[{"type":"strong"}],"text":"奖励交互式线程、惩罚占用 CPU 的线程"},{"type":"text","text":"。在 Linux O(1) 调度器中,最高的优先级奖励是 -5,注意这个优先级越低越容易被线程调度器接受,所以最高惩罚的优先级是 +5。具体体现就是操作系统维护一个名为 "},{"type":"codeinline","content":[{"type":"text","text":"sleep_avg"}]},{"type":"text","text":" 的变量,任务唤醒会增加 sleep_avg 变量的值,当任务被抢占或者时间量过期会减少这个变量的值,反映在奖励机制上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 调度算法是 2.6 内核版本的调度器,最初引入这个调度算法的是不稳定的 2.5 版本。早期的调度算法在多处理器环境中说明了通过访问正在活动数组就可以做出调度的决定。使调度可以在固定的时间 O(1) 完成。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 调度器使用了一种 "},{"type":"codeinline","content":[{"type":"text","text":"启发式"}]},{"type":"text","text":" 的方式,这是什么意思?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">在计算机科学中,启发式是一种当传统方式解决问题很慢时用来快速解决问题的方式,或者找到一个在传统方法无法找到任何精确解的情况下找到近似解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 使用启发式的这种方式,会使任务的优先级变得复杂并且不完善,从而导致在处理交互任务时性能很糟糕。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了改进这个缺点,O(1) 调度器的开发者又提出了一个新的方案,即 "},{"type":"codeinline","content":[{"type":"text","text":"公平调度器(Completely Fair Scheduler, CFS)"}]},{"type":"text","text":"。 CFS 的主要思想是使用一颗"},{"type":"codeinline","content":[{"type":"text","text":"红黑树"}]},{"type":"text","text":"作为调度队列。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">数据结构太重要了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 会根据任务在 CPU 上的运行时间长短而将其有序地排列在树中,时间精确到纳秒级。下面是 CFS 的构造模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/20/2088e1107f6f8d8382b067c0692006ec.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 的调度过程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 算法总是优先调度哪些使用 CPU 时间最少的任务。最小的任务一般都是在最左边的位置。当有一个新的任务需要运行时,CFS 会把这个任务和最左边的数值进行对比,如果此任务具有最小时间值,那么它将进行运行,否则它会进行比较,找到合适的位置进行插入。然后 CPU 运行红黑树上当前比较的最左边的任务。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在红黑树中选择一个节点来运行的时间可以是常数时间,但是插入一个任务的时间是 "},{"type":"codeinline","content":[{"type":"text","text":"O(loog(N))"}]},{"type":"text","text":",其中 N 是系统中的任务数。考虑到当前系统的负载水平,这是可以接受的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调度器只需要考虑可运行的任务即可。这些任务被放在适当的调度队列中。不可运行的任务和正在等待的各种 I/O 操作或内核事件的任务被放入一个"},{"type":"codeinline","content":[{"type":"text","text":"等待队列"}]},{"type":"text","text":"中。等待队列头包含一个指向任务链表的指针和一个自旋锁。自旋锁对于并发处理场景下用处很大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Linux 系统中的同步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面来聊一下 Linux 中的同步机制。早期的 Linux 内核只有一个 "},{"type":"codeinline","content":[{"type":"text","text":"大内核锁(Big Kernel Lock,BKL)"}]},{"type":"text","text":" 。它阻止了不同处理器并发处理的能力。因此,需要引入一些粒度更细的锁机制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 提供了若干不同类型的同步变量,这些变量既能够在内核中使用,也能够在用户应用程序中使用。在地层中,Linux 通过使用 "},{"type":"codeinline","content":[{"type":"text","text":"atomic_set"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"atomic_read"}]},{"type":"text","text":" 这样的操作为硬件支持的原子指令提供封装。硬件提供内存重排序,这是 Linux 屏障的机制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具有高级别的同步像是自旋锁的描述是这样的,当两个进程同时对资源进行访问,在一个进程获得资源后,另一个进程不想被阻塞,所以它就会自旋,等待一会儿再对资源进行访问。Linux 也提供互斥量或信号量这样的机制,也支持像是 "},{"type":"codeinline","content":[{"type":"text","text":"mutex_tryLock"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"mutex_tryWait"}]},{"type":"text","text":" 这样的非阻塞调用。也支持中断处理事务,也可以通过动态禁用和启用相应的中断来实现。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 启动"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面来聊一聊 Linux 是如何启动的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当计算机电源通电后,"},{"type":"codeinline","content":[{"type":"text","text":"BIOS"}]},{"type":"text","text":"会进行"},{"type":"codeinline","content":[{"type":"text","text":"开机自检(Power-On-Self-Test, POST)"}]},{"type":"text","text":",对硬件进行检测和初始化。因为操作系统的启动会使用到磁盘、屏幕、键盘、鼠标等设备。下一步,磁盘中的第一个分区,也被称为 "},{"type":"codeinline","content":[{"type":"text","text":"MBR(Master Boot Record)"}]},{"type":"text","text":" 主引导记录,被读入到一个固定的内存区域并执行。这个分区中有一个非常小的,只有 512 字节的程序。程序从磁盘中调入 boot 独立程序,boot 程序将自身复制到高位地址的内存从而为操作系统释放低位地址的内存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"复制完成后,boot 程序读取启动设备的根目录。boot 程序要理解文件系统和目录格式。然后 boot 程序被调入内核,把控制权移交给内核。直到这里,boot 完成了它的工作。系统内核开始运行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内核启动代码是使用"},{"type":"codeinline","content":[{"type":"text","text":"汇编语言"}]},{"type":"text","text":"完成的,主要包括创建内核堆栈、识别 CPU 类型、计算内存、禁用中断、启动内存管理单元等,然后调用 C 语言的 main 函数执行操作系统部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这部分也会做很多事情,首先会分配一个消息缓冲区来存放调试出现的问题,调试信息会写入缓冲区。如果调试出现错误,这些信息可以通过诊断程序调出来。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后操作系统会进行自动配置,检测设备,加载配置文件,被检测设备如果做出响应,就会被添加到已链接的设备表中,如果没有相应,就归为未连接直接忽略。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"配置完所有硬件后,接下来要做的就是仔细手工处理进程0,设置其堆栈,然后运行它,执行初始化、配置时钟、挂载文件系统。创建 "},{"type":"codeinline","content":[{"type":"text","text":"init 进程(进程 1 )"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"守护进程(进程 2)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"init 进程会检测它的标志以确定它是否为单用户还是多用户服务。在前一种情况中,它会调用 fork 函数创建一个 shell 进程,并且等待这个进程结束。后一种情况调用 fork 函数创建一个运行系统初始化的 shell 脚本(即 /etc/rc)的进程,这个进程可以进行文件系统一致性检测、挂载文件系统、开启守护进程等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后 /etc/rc 这个进程会从 /etc/ttys 中读取数据,/etc/ttys 列出了所有的终端和属性。对于每一个启用的终端,这个进程调用 fork 函数创建一个自身的副本,进行内部处理并运行一个名为 "},{"type":"codeinline","content":[{"type":"text","text":"getty"}]},{"type":"text","text":" 的程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getty 程序会在终端上输入 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"login:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"等待用户输入用户名,在输入用户名后,getty 程序结束,登陆程序 "},{"type":"codeinline","content":[{"type":"text","text":"/bin/login"}]},{"type":"text","text":" 开始运行。login 程序需要输入密码,并与保存在 "},{"type":"codeinline","content":[{"type":"text","text":"/etc/passwd"}]},{"type":"text","text":" 中的密码进行对比,如果输入正确,login 程序以用户 shell 程序替换自身,等待第一个命令。如果不正确,login 程序要求输入另一个用户名。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整个系统启动过程如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/df/df9efb39c4f1c9e6ad46db87da8d6f30.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cd6da7c74de719095421f0294e6c1450.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章