12

12.进程——程序是如何启动的

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU1NTM0NDEzNw%3D%3D&%3Bmid=2247483826&%3Bidx=1&%3Bsn=cb4c323d87fe77d6ea8206a51a9d17b6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

代码编译出来后得到可执行文件,也就是我们常说的程序。都说进程是运行起来的程序,那 程序是如何启动的呢?程序和进程究竟有哪些区别呢?

早期的计算机只运行一个程序,所以可以想到的是 CPU 的利用率是多么的低,为了让 CPU 利用率得到有效的提高,工程师们设计了一个方案:加载多个程序到内存中,让它们并发运行,而操作系统控制着这些程序运行状态,这些运行的程序就是我们所谓的进程。虽然有多个程序并发执行任务,但 每个进程在运行过程中都会认为自己独占 CPU  资源、内存资源等,以为整个计算机的硬件资源都是它的 。这是操作系统的设计哲学:进程你尽管大胆的往前跑,你要什么我都给你准备好了,你跑起来就对了。

还记得之前有篇是专门计算机是如何启动的吗?没有看过的同学可以看这里 3.你知道计算机是如何启动的吗? 。当 BIOS 固件将操作系统内核装载进内存执行后产生了第一个进程,一般来说将这个进程称为进程 0,它的作用是对所有内核核心数据结构进行预先赋值。紧接着创建出一个 init 进程。进程 0 是所有进程的祖先,其他进程要么是它的子进程,要么是它的子孙进程。既然内核进程有 BIOS 固件帮它启动,普通进程是如何启动的呢?

在 linux 系统中提供了两个重要的系统调用(系统函数): fork()、exec() ,利用好这两个函数就可以启动一个进程。步骤是这样的:

先用 fork() 函数创建一个新的进程 。注意:这个新的进程比较特殊!它会 将父进程的资源内存地址都复制到自己的内存空间 ,也就是说,它会和父进程共享父进程的资源。有人就问了:哪来的父进程?为什么要共享父进程的资源?第一个问题:谁来创建出这个新进程就是父进程,比如进程 1 是进程 0 创建出来,那进程 0 就是进程 1 的父进程,有没有进程是没有父进程的?有,且只有一个,就是进程 0,其他都是有父进程的,它们的关系就如同一棵树一样。

773MZj.jpg!mobile

第二个问题:在早期的 linux 系统实现中,创建新的进程是会复制父进程的资源的,因为创建出来的子进程需要分配各种资源,比如进程描述符、地址空间等,如果不从父进程复制过来,那应该如何初始化这个新的进程呢?总不能给它一个空的结构吧,所以 最方便省事的方式就是把父进程的东西给复制过来 注意,复制父进程的资源与复制父进程资源内存地址是不一样的 。前者是所有的资源再次创建,后者是直接利用指针进行赋值,前者会在系统中增加一份与父进程一模一样的资源,后者不会增加资源。现在的操作系统都是后者,这也是现代操作系统的一个优化点,采用的是一个叫 写时复制 (COW,Copy On Write)的技术。为什么要这么做呢?你想,每次创建新进程都要把父进程的资源复制一遍,这得多 耗费 CPU 和内存资源 ,而且新进程还不一定用的到父进程的资源,而且根据统计大部分情况下都是用不到的。所以如果只用指针去进行复制资源是一个很好的方式, 减少了很多不必要的资源损耗

当子进程通过 fork 创建出来后,下一步需要进行 exec。 这步会将你指定的子进程程序加载到这个新的进程内存空间中并执行。 这两步都完成后才算完成了子进程的启动和运行。

下面是一个关于利用 fork 和 execve 创建一个新进场的示例。fork 会先创建出一个新的进程,然后利用 execve 装载另外一个程序,这里用的 echo 程序向控制台打印 hello。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>


int main(int argc, char *argv[]){
char *arg[] = {"echo", "hello", NULL};


if(fork() == 0){
printf("child process\n");
if(execve("/usr/bin/echo", arg, NULL) == 0){
exit(1);
}
} else {
printf("parent process exit\n");
return EXIT_SUCCESS;
}
}

fork() 的返回值有三种:

  • -1:表示 fork 系统调用出错。

  • 0:表示 fork 系统调用成功,现在处于子进程内。

  • 正数:表示处于父进程内。

所以你们会发现在示例代码里会有一个 fork() == 0 的判断,利用这个可以是否当前是否是在新的子进程内。

我们都知道进程是运行起来的程序,但其实进程包括很多东西,比如:

  • 进程的运行状态 :包括就绪、运行、等待/阻塞、僵尸等。

  • 程序计数器 :记录当前进程运行到哪条指令了。

  • CPU 寄存器 :保存进程运行的上下文信息,以便当前进程调度出去后还能调度回来接着运行。

  • CPU 调度信息 :包括进程优先级、调度队列、调度等。

  • 内存信息 :进程使用的内存信息,如页表等。

  • 文件信息 :进程打开的文件信息。

  • 资源限制信息 :CPU、内存、带宽等限制的信息,如:可以限制进程运行所能使用的 CPU 核数。

我们可以把程序看做是一个人,人活着就相当于一个进程。 比如:进程需要 CPU,人活着需要食物;进程需要内存,人活着要房子;进程要与其他进程交流的话,需要管道、socket 等,人与人交流需要手机等;进程竞争 CPU,人竞争食物。 很多时候操作系统中的很多东西都可以类比人类社会中的某些方面,可能是工程师们在设计操作系统时参照了人类社会吧。

欢迎关注公众号:哈扣。

MJfy22V.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK