前面写了7篇文章介绍如何下载和编译,以及如何调试 linux 内核。在此基础上,现在开始,将和大家一起学习 linux 内核的基本设计和实现。本节先来学习一下 linux 内核中的进程。
进程不仅是 linux,也是大多数现代操作系统中的基本概念,程序员编写的代码编译成程序存储在磁盘中,而“进程”则可以认为是执行期的程序。但是进程又不仅仅是程序,它还包含其他资源,比如打开的文件,挂起的信号,分配的内存,占用的处理器资源等等。进程实际上是各种资源混杂的集合,linux 内核需要有效的清晰的管理进程。
线程则是在进程中活动的对象,每个线程都拥有独立的程序计数器,栈空间和各种进程寄存器。linux 内核调度的对象是以线程为基本单位的,而不是进程。在早期的 unix 系统中,一个进程只包含一个线程,而现在包含多个线程的进程已经非常常见了。不同于 windows 等操作系统,linux 并不严格区分线程和进程,对于 linux 操作系统而言,线程只是一种特殊的进程而已。这是一种哲学,linux 将线程包含在进程里的这种设计实现,反而更加简洁稳健。
现代操作系统中一般都会有虚拟处理器和虚拟内存两种虚拟概念,linux 也不例外。在 linux 系统中,可能同时有多个进程在执行,即使系统只有一个 cpu,因为虚拟处理器的设计,所有进程都会觉得自己在独占 cpu。虚拟内存的概念也是类似的,也许系统只有 1GB 的内存,但是对于进程而言,它们都会认为自己在使用 1GB 的内存。
所以,程序本身并不是进程,进程是处于执行期的程序和它占有资源的集合,可能多个进程实际上都是由一个程序而来。例如同一个程序执行两次,如果它们都没有退出,则就在 linux 系统中产生了两个进程。
linux 操作系统中的进程有着明显的“父子关系”。在第三节,我们已经知道 linux 内核在启动后期,会执行 init 程序,如果一切正常的话,将产生 linux 系统中的第一个进程,该进程会 fork 出若干子进程,子进程会继续 fork 出子进程。所以,可以说 linux 系统中的 init 进程是所有其他进程的父进程。
linux 内核通过唯一的进程标识 pid 来表示每个进程,pid 其实就是一个 int 类型的数,它的最大值通常默认为 32768,也就是说如果 linux 系统中产生过 32768 个进程,下一次再产生新的进程时,它的 pid 数将从头开始了。虽然 linux 内核可以循环利用 pid,但是 pid 的最大值依然很重要。这个值越小,转一圈就越快,本来 pid 大的进程比 pid 小的进程迟运行,现在就破坏了这一规则。不过可以通过修改
/proc/sys/kernel/pid_max
来提高上限。
了解了linux内核中的pid,那 init 进程是最早执行的,它应该有最小的 pid 值了?的确如此,在 linux 输入 ps 命令,即可查看这一事实:
linux 内核在启动时,父进程通常使用 fork() 系统调用创建子进程的,这个函数可以将父进程复制一份,用于创建子进程。fork() 函数不同于一般的 C 语言函数,它会从内核返回两次,一次回到父进程,一次回到子进程。在 linux 中输入 man fork 即可查看 fork 函数的详细说明:
作为使用者,我们更关心的是它的返回值:
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the
parent, no child process is created, and errno is set appropriately.
大意是,如果 fork 函数执行失败,则会返回 -1。否则,将在子进程中返回 0,父进程中返回子进程的 pid。来个实例吧,下面是使用 fork 函数的一个 C 语言 demo:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pid, status;
printf("main start...\n");
pid = fork();
switch(pid){
case -1:
printf("fork failed\n");
exit(-1);
case 0:
printf("this is child, pid=%d\n", getpid());
sleep(10);
return 110;
default:
printf("this is father, pid=%d\n", getpid());
wait(&status);
printf("father: child exit with status %d\n", WEXITSTATUS(status));
return 0;
}
return 0;
}
如果代码比较乱,可以看图:
程序很简单,就是分别在父子进程中打印出自己的 pid,子进程然后睡眠 10 秒钟,返回 110。父进程使用 wait 函数等待子进程结束,并接收子进程的返回值,最后再打印出来。编译并执行,得到如下结果:
# gcc t.c
# ./a.out
main start...
this is father, pid=23798
this is child, pid=23799
father: child exit with status 110
这其实也是一个程序产生两个进程的例子,在程序睡眠期间,我们使用 ps 命令查看 a.out 的 pid,实际上也能找到两个进程:
# ps -A|grep a.out
23798 pts/5 00:00:00 a.out
23799 pts/5 00:00:00 a.out
就先介绍到这里吧。