操作系统-15-进程的创建

从这一节起,我们将详细讲解进程的一生。进程如人生,进程的一生同样包含三个阶段,创建,运行和终结,本节是进程三部曲的开篇:进程创建。

接下来,我们讲解关于进程创建的诸多问题。

进程是由谁创建的?在什么情况下创建的?
幸好这个问题不像鸡生蛋蛋生鸡那样,这个问题的答案相对简单,进程的创建者有两种:

1.操作系统可以创建新的进程
2.进程也可以创建新的进程

其中,进程的创建者被称为父进程,创建出的进程被称为子进程。

1,操作系统创建进程:初始化

作为计算机的Boss,最初的进程是由操作系统创建的,操作系统在初始化的过程中会创建一系列进程。这些进程中有的是用户可见的,主要用来和用户进行交互,比如Windows或Linux开机后输入用户密码,这就是一个进程,这类进程被称之为前端进程(Foreground Processes);

有的是用户看不到的在背后默默运行的进程,比如用来检测系统是否有更新的进程,这类进程被称之为后端进程(Background Process)。在Windows或Linux系统中,虽然开机后我们没有打开任何程序,但是使用Windows下的任务管理器或者Linux中的ps命令,你都会发现操作系统已经创建了很多进程。

2,进程创建进程:系统调用

其实从本质上来说,进程最终都是由操作系统创建出来的,但是操作系统把创建进程作为一项服务提供给了用户程序。还记得程序员应该怎样向操作系统请求服务吗,没错,就是通过系统调用。用户程序可以通过系统调用来创建新的进程。在Linux(Unix)下这个系统调用是大名鼎鼎的fork,在Windows下这个系统调用叫做CreateProcess。

比如在Linux系统中,我们通过命令行来运行程序,其实命令行解释器,比如常用的bash,也是一个进程,bash进程等待用户输入,然后调用系统调用创建新的进程来执行命令。比如我们常用的ps命令,注意ps本身就是一个可执行程序,当用户敲击回车按键后,bash调用fork创建新的进程,这个新的进程运行的就是ps程序,bash本身不关心ps进程是如何工作的,bash要做的就是等待,等待ps运行完成并输出结果后返回到bash,然后等待用户的下一次输入,如图所示:
在这里插入图片描述

从操作系统使用者的角度来说,一切皆为进程。操作系统中你能看到的、能用的都是进程。

因此,学习操作系统要以进程为核心,须知,操作系统中的一切都是为进程来服务的。

程序员什么情况下需要创建进程
作为程序员,我们在什么情况下需要使用进程呢?顺便说下,这也是笔者经常使用的一个面试题 😃

接下来从三个角度来讲解:任务处理、充分利用多核、增强系统稳定性。

1,任务处理

在这里我们以Unix系统为例来说明。Unix中大量使用了进程,Unix哲学之一就是:

一个程序只需要做一件事并且做到最好

让这些程序可以协同工作,由用户决定如何把这些小的程序组合来完成复杂的任务。比如在Unix下,我们需要去文件夹中找到所有包含字符串“anetos”的文件并把这些文件按照名称排序显示出来,那么在Unix下你就可以这样来完成任务:grep -r anetos * | sort 。其中grep程序用来查找有哪些文件包含了字符串“anetos”,得到这些文件后把文件名作为输入传递给sort程序,sort排序后显示出来,“|”表示grep的输出用作sort的输入,如图所示,bash这个进程使用fork创建出来了两个子进程,一个是grep,另一个是sort,这两个进程通力合作完成了我们的任务。

在这里插入图片描述
因此,当任务可以轻易的分解为几个独立而又相互关联的几部分时,进程就非常有用了,每一部分任务都可以作为一个独立的进程。还是以上图“搜索文件并排序”为例,bash、grep、sort几个程序彼此独立,bash用来执行用户命令,grep用来搜索,sort用来排序,这几个程序相互独立没什么关系,但是当需要完成“搜索文件并排序”这样的任务时,这几个程序就有关联了,grep可以搜索出需要的文件,sort可以对文件名进行排序,而bash可以创建grep进程以及sort进程来完成用户任务。注意这里bash自己本身并不去真正的执行任务,bash这这项工作交给了grep和sort来完成。当然我们也可以写一个程序,把grep的搜索功能和sort的排序组合在一个程序里,但是,类似这种功能大而全的程序往往非常复杂难以维护,而且灵活性较差。因此,在这里你可以看到bash,grep,sort就像积木模块一样,彼此独立,但是又能灵活组合,这种模块化设计在计算机科学中是一种极其强大的思想。

其实你可以把多个进程协同完成一项任务想象成这样一个过程:假设你的老板交给你一项任务,而作为主管的你肯定不会事事亲力亲为,因此你把这项任务拆分成了三个部分并找到得力的三个小兵,分别让他们去执行各自的任务并告诉他们完成后汇报给自己。在这个例子中,这三个小兵就好比创建出来的进程(子进程),作为主管的你就是创建进程的进程(父进程)。

2,充分利用多核

在多核系统中(也就是有多个CPU的计算机中),多个进程可以运行在不同的CPU上,这显然会加快处理速度,因为这些进程是真正的同时运行,也就是我们常说的并发。

早期的操作系统是不支持线程的,在这里我们也暂时不考虑多线程的情况 ,稍后的课程中我们会详细讲解线程。

在不考虑多线程的情况下,一个进程在某一时刻只能在一个CPU上运行,注意这里的意思不是说一个进程只能一直在某一个CPU上运行,当进程被暂停并重新运行后,该进程可能会在另一个CPU上被执行,这就是进程调度,CPU从执行线程A切换到线程B的过程被称为进程切换(Context Switch)。

假如我们的系统中有四个CPU,为完成某项任务我们创建了一个进程,那么该进程就只能在一个CPU上运行,即使其它三个CPU当前无事可做,因此在这种情况下,我们没有充分利用CPU资源。

在这里插入图片描述
但是如果我们再额外创建出三个进程,并把任务分配给这些进程的话,情况就不同了,如图所示:
在这里插入图片描述
在这种情况下,父进程以及被创建出来的三个子进程在四个CPU上并行执行(注意,由于有四个CPU,因此这四个进程是真正的并行执行),不会出现某个CPU过于繁忙而其它CPU过于空闲的情况。我们使用多进程技术充分利用了系统中的四个CPU,从而加快了任务处理速度。所谓“多进程”,也就是创建多个进程协作完成任务。

虽然多进程可以充分利用多核,但是,像任何事物一样,多进程同样存在缺点,那就是进程切换的代价比较大(我们将会在本章稍后的部分详细讲解进程切换),进程切换过程会消耗较多的CPU时间,而CPU在这段时间没有在执行有用的任务。为解决这个问题,现代操作系统都开始支持线程(Thread),我们将在本书“程序员应如何理解线程”这一章中进行详细讲解。

尽管多进程技术有切换代价较大的缺点,但是进程依然凭借其独特的能力在程序设计领域占有一席之地。接下来,就让我们看看这项独特的本领。

3,增强系统稳定性

进程的这项独特本领就是稳定性较高。不管在任何领域,系统的稳定性都是工程师们不断努力去实现的,软件工程也不例外,而进程是实现系统稳定性的一项技术。

为什么进程会相对稳定呢?原因就在于一个进程崩溃终结后不会影响任何其它进程,其原因在于每个进程都是独立的、互不干扰,每个进程都有自己的地址空间。所谓每个进程都有自己地址空间,指的是进程在自己内存中的读写操作不会影响到其它进程,正是由于每个进程都有自己独立的地址空间才使得各个进程相互隔离互不干扰(关于这一点,我们将在“操作系统如何管理内存”这一章中看到操作系统是如何做到的)。

而与多进程相对比的多线程则无此优点,多个线程其实是共享同一个进程的地址空间的,比如线程A、线程B、线程C属于进程P,意思是在进程P中创建的线程A、B、C,那么线程A、B、C共享进程P的地址空间。线程间共享地址空间有一个很大的优点,那就是一个线程对内存的读写进程中的其它线程都能看到这一内存操作,这就是是的线程间通信是极为方便的。但是这种便利性也是有代价的,那就是线程间通信极为方便但想正确的进行通信却非常困难,原因就在于多线程编程会带来棘手的Race Condition的问题,让人头晕脑胀的锁就是来解决这一问题的,我们在后面的课程中还会回到这一主题。

这种便利性的另一个问题就是当某个线程崩溃后,该线程所在的整个进程都会退出,当然进程中的所有线程也都不复存在。因此你会看到,同多线程相比,多进程有很好的系统稳定性,父进程创建的子进程崩溃后不会影响到其它子进程以及父进程。

因此在对系统稳定性较高的领域,比如Web服务器Nginx,会经常见到多进程技术的使用,这正是利用了进程相互隔离互不干扰的优点。

在了解了进程创建的Who、When、Why几个问题后,接下来我们依然以Unix系统为例看一下如何从进程的角度来理解操作系统。

从进程的角度来理解操作系统

Unix系统中的进程和人颇有几分相似之处。

每个进程在系统中都有自己唯一的id,通常是一个正整数,用来识别进程,这个id被称之为进程描述符(Process Identifier),简称pid。pid之于进程就好比身份证号之于人一样,通过身份证号就可以唯一确定某个人;同样,通过pid,我们就可以唯一确定一个进程。

Unix中创建进程的进程被称之为父进程,被创建的进程被称为子进程。就像人类的子女会继承父母的基因一样,子进程同样会继承父进程的部分资源,比如一部分内存空间以及父进程打开的文件等,这一点我们不在这里展开,具体内容会在“操作系统如何管理内存”和“操作系统如何管理文件”两章中详细讲解,让我们把注意力先放在进程上面。

同父进程一样,被创建出来的子进程同样也可以创建进程,因此,你会发现最终这些进程会形成一种类似于树的关系,叫做进程树,就像人的族谱一样。Unix中有专门的命令可以按照树的形式打印出系统中所有进程的关系,比如pstree。

下图就是Unix系统运行起来后形成的一个典型的进程树,注意这里只显示了部分进程。从图中你可以看到init进程是所有进程的祖先,Unix系统启动之后创建init进程(操作系统创建进程),init进程运行起来后开始创建一系列子进程(进程创建进程),比如这里的login进程,kthreadd进程,sshd进程。login进程用来管理登录到该系统的用户,用户成功登陆后login进程创建bash进程,bash进程就是我们熟悉的命令行界面了;kthreadd进程本身同样创建一系列进程,注意这些进程工作在内核模式,也就是说这些进程是操作系统的一部分(注意操作系统自己也可以创建属于自己的进程);sshd进程用来管理ssh链接,当我们使用ssh命令远程登录时,远端计算机上就是sshd这个进程来管理ssh链接的,链接成功后,sshd创建bash进程,这样我们就可以通过ssh来进行远程控制了。在Unix下你可以通过使用ps命令看到这些进程。

在这里插入图片描述
在这个例子中可以看到,如果从进程的角度出发,其实我们可以更加容易的理解操作系统在做些什么。操作系统中无非就是有一堆进程,有的进程工作在用户模式,执行用户程序;有的工作在内核模式,是操作系统创建出来完成特定任务的进程,比如进行管理磁盘,管理网卡等。当网卡中断产生后,操作系统(具体是操作系统中的调度器)决定暂停当前进程转而去执行网卡管理进程,网卡管理进程接收到网络数据后,等待网络数据的用户进程就可以继续运行了。

因此以进程这个角度我们可以这样来看待操作系统,如下图所示:真正用来执行具体任务的是进程,这些进程分为两部分,一部分工作在用户模式,代表用户,比如用来编辑代码的vim进程、用来编译程序的gcc进程、用来查阅资料的浏览器进程、听音乐的播放器进程等等;另一部分工作在内核模式,代表操作系统,比如进行内存管理、设备管理等。而决定哪个进程运行的是调度器,决定调度器是否运行的就是中断。需要注意的是,该图仅仅用于帮助我们从进程的角度来理解操作系统,真实的操作系统中很少会有这样清晰的划分。
在这里插入图片描述
接下来我们依然以Unix为例来实例讲解进程创建。

实例讲解进程创建:fork与exec
Unix世界中使用系统调用fork来创建进程,fork是一个很有趣的函数,该函数有两种返回值,一种返回值用来表示接下来的代码将会在父进程中被执行;另一种返回值表示接下来的代码将会在子进程中被执行。如代码所示,编译后的可执行程序我们命名为anetos:

include <sys/types.h>
include <stdio.h>
include <unistd.h>
int main()
{
pid_t pid;

/* 使用系统调用fork创建新进程 */
pid = fork();
if (pid < 0) { /* 调用失败 */
    printf("调用失败.\n");
    return 1;
}
else if (pid == 0) { /* 接下来就是子进程 */
    printf("我是子进程.\n");
    execlp("/bin/ls","ls",NULL);
}
else { /* 父进程,接下来的代码依然在该进程中执行 */
    printf("我是父进程.\n");
    /* 等待子进程执行完成 */
    wait(NULL);
    printf("子进程执行完成.\n");
}
return 0;

}
注意,虽然printf(“我是子进程.\n”)以及printf(“我是父进程.\n”)这两段代码写在同一个源文件中,但是,注意这两段代码实际上运行在两个不同的进程当中,父进程与子进程。此外,子进程的内存其实是用父进程的内存来初始化的,换句话说就是fork系统调用执行创建子进程后,子进程的内存其实是父进程的一个拷贝,如下图所示:
在这里插入图片描述
父进程和子进程的内存布局是一样的,不但一样,而且大部分是共享的。有的同学可能会有疑问,不是说不同的进程是互相独立而且相互隔离的吗?没错,fork系统调用之后,虽然子进程内存的内容是从父进程中copy过来的,但是,父子进程的地址空间是完全不一样的,也就是说fork系统调用之后父子进程内存的内容一样但二者位于完全不同的地址空间,之前对进程独立性的描述依然成立,也许你现在对此感觉难以理解,学完“操作系统如何管理内存”这一章你就都能明白了。

只要父子进程不对同一块内存进行写操作,那么这块内存就会一直共享下去,一旦父进程或子进程改变某一块内存,那么这块内存将不在被父子进程共享,而是父子进程各有自己不同的版本,这就是写时复制技术Copy On Write,由于该技术涉及到内存,我们将在“操作系统如何管理内存”这一章中再次回到这一话题。你会看到在Unix中用fork进行进程创建是非常快速的,因为子进程会共享大部分父进程内存,然后使用Copy On Write技术保持进程独立性。父子进程双方对内存的读写不会影响到对方。

现在我们知道了fork系统调用后会创建出一个和父进程一样的子进程,一般来说,创建出一个和父进程相同的子进程是没什么用的。因此通过fork创建出子进程后,子进程通常会调用另一个系统调用exec来执行其它程序,比如子进程通过exec系统调用开始运行/bin/ls程序。exec系统调用会将新的程序加载到内存当中,这样一个新的程序就在子进程的内存中运行起来了,如图所示:
在这里插入图片描述
在Unix世界中,fork和exec是创建进程必备的两个系统调用。如果你工作在Unix(Linux)平台下,那么这两个系统调用是你必须要熟悉的。

总结
在进程三部曲的开篇,我们详细讲解了谁来创建进程,如何创建进程。作为程序员我们需要充分利用进程这一强大的武器来完成我们的任务,因此在这一节中我们从三个角度讲解了程序员在什么情况下需要创建进程。

操作系统是复杂的,但是从进程的角度我们可以更加容易的理解操作系统,因此我们在这一节中讲解了如何从进程的角度来理解操作系统。

最后我们用了一个实际的例子来讲解了如何用系统调用fork和exec来创建新的进程。

接下来让我们聆听进程三部曲的中篇,生命的乐章之进程运行,理解一下进程是如何运行的。

本文来自《码农的荒岛求生》

  • 26
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发如雪-ty

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值