进程控制(粗略概括)

1. 有关进程

1.1 什么是进程

我们在前面的课程就讲过这个问题,我们这里再来回顾下。

我们说,将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程。

对比一下进程和程序:

存储位置 存在状态 运行过程
程序 硬盘 静态的 无运行的过程
进程 存在内存中,它是从磁盘上的程序考过来的副本 动态的 有运行的过程,所以进程有生有死

1.2 多进程并发运行

有OS支持时,会有很多的进程在运行,这些进程都是并发运行的。

什么是并发运行?
就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复,由于时间片很短,
在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一个进程的指令。

图:

当然我们这里说的单核cpu的情况,如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,这种叫并行运行。

所以当cpu是多核时,并发与并行是同时存在的。

1.3 进程ID(PID)

1.3.1 什么是PID
基于OS运行的进程有很多,OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID,
P就是process——进程的意思。

这记好比公安局给每个人分配了一个唯一的身份证号(ID)是一样的。
ps查看:

如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的。

因为ID的唯一性,当我们想创建一个名字唯一的文件时,往往可以在文件名中加入PID,这样就能保证文件名的唯一性。

1.3.2 那么PID放在了那里呢?

进程在运行的过程中,OS会去管理进程,这就涉及到很多的管理信息,OS(Linux)为了管理进程,会为每一个进程创建一个
task_struct结构体变量,里面放了各种的该进程的管理信息,比如第一章介绍的文件描述符表,又比如我们这里讲的PID。

所以PID放在了该进程的task_struct结构体变量中,有关task_struct在前面的课程就介绍过,相信大家不会陌生。

1.3.3 如何获取PID呢?
后面回答这个问题。

1.4 三个特殊的进程

OS运行起来后有三个特殊的进程我们需要了解下,他们的PID分别是0、1、2。

0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行,尽管我们总是忽略它们的存在,但是它们
确非常的重要。

1.4.1 进程 PID == 0 的进程

(1)作用
这个进程被称为调度进程,功能是实现进程间的调度和切换,该进程根据调度算法,该进程会让CPU轮换的执行所有的进程。

怎么实现的?
当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换。

(2)这个进程怎么来的
这个进程就是有OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0的进程。

由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。

1.4.2 进程ID == 1的进程

(1)作用
1)作用1:初始化
这个进程被称为init进程,这个进程的作用是,他会去读取各种各样的系统文件,使用文件中的数据来初始化OS的启动,
让我们的OS进入多用户状态,也就是让OS支持多用户的登录。

2)作用2:托管孤儿进程
什么事孤儿进程,怎么托管的,有关这个问题后面会详细介绍。

3)作用3:原始父进程

原始进程————>进程————————>进程————————>终端进程——————>a.out进程
| | |
| | |
V V |
进程 进程 进程
| | |
| | |
... ... ...

(2)这个进程怎么运行起来的
这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序,程序代码放在了
/sbin/init下,当OS启动起来后,OS回去执行init程序,将它的代码加载到内存,这个进程就运行起来了。

1.4.3 进程ID == 2的进程

(1)作用
页精灵进程,专门负责虚拟内存的请页操作。

疑问:什么精灵进程?
精灵进程也叫守护进程,我们后面讲到“守护进程”这一章时,你自然就知道了。

怎么理解换页操作,我们说当OS支持虚拟内存机制时,加载应用程序到内存时,并不会进行完整的代码拷贝,只会拷贝当前要运
行的那部分代码,当这部分代码运行完毕后,会再拷贝另一部分需要运行的代码到内存中,拷贝时是按照一页一页来操作的,
每一页大概4096字节,这就是换页操作。

想了解详细换页操作的同学,请看《计算机体系结构》软件篇4——操作系统部分的课程。

(2)这个进程怎么运行起来的
与调度进程一样,也是一个系统进程,代码属于OS的一部分。

1.5 获取与进程相关的各种ID的函数

1.5.1 函数原型和所需头文件
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
gid_t getgid(void);

(1)功能
1)getpid函数:获取调用该函数进程的进程ID。
2)getppid函数:获取调用该函数进程的父进程ID,第一个P是parent,第二个process。
3)getuid函数:获取调用该函数进程的用户ID。
在什么用户下运行的该进程,得到的就是该用户的用户ID,查看/etc/passed文件,可以找到该UID对应的用户名。

4)getgid函数:获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID。

(2)返回值:返回各种ID值,不会调用失败,永远都是成功的。

1.5.2 代码演示

2. 程序的运行过程

2.1 程序如何运行起来的

(1)在内存中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
(3)pc指向第一条指令,cpu取指运行

当有OS时,以上过程肯定都是通过调用相应的API来实现的。
在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec。

fork:开辟出一块内存空间
exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了
运行起来的进程会与其它的进程切换着并发运行。

2.2 fork
2.2.1 函数原型
#include <unistd.h>

pid_t fork(void);

为了便于大家更容易的理解,我们在介绍fork时会适当的隐去一些信息,所以虽然不能保证100%是正确的,但是我们能够向
大家解释清楚fork函数的作用。

(1)函数功能
从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。

复制后有两个结果:
1)依照父进程内存空间样子,原样复制地开辟出子进程的内存空间
2)由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程完全相同

其实复制父进程的主要目的,就是为了复制出一块内存空间,只不过复制的附带效果是,子进程原样的拷贝了一份
父进程的代码和数据,事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码。

(2)函数参数:无参数。
(3)函数返回值
由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数,当然这个说法有些欠妥,但是暂且这么理解。

1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
2)子进程的fork,成功返回0,失败返回-1,errno被设置。

(4) 代码演示

如何让父子进程做不同的事情?

2.2.2 说说复制的原理
Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的
对应的还是物理内存。

复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存
空间,里面放了与父进程一模一样代码和数据,
图:

如果想了解什么是虚拟内存,请看《计算机体系结构》软件篇4——操作系统。

2.2.3 父子进程各自会执行哪些代码

复制出子进程后,父子进程各自都有一份相同的代码,而且子进程也会被运行起来,那么我们来看一下,父子进程各自会执行
哪些代码。

图:

代码验证:

(1)父进程

1)执行fork前的代码
2)执行fork函数
父进程执行fork函数时,调用成功会返回值为子进程的PID,进入if(ret > 0){}中,执行里面的代码。
if(ret > 0){}中的代码只有父进程才会执行。

3)执行fork函数后的代码

(2)子进程
1)fork前的代码
尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行。

2)子进程调用fork时,返回值为0,注意0不是PID。
进入if(ret == 0){},执行里面的代码。

if(ret == 0){}中的代码只有子进程执行。

3)执行fork后的代码

(3)验证子进程复制了父进程的代码和数据
演示:

2.3 父子进程共享操作文件

(1)情况1:独立打开文件
多个进程独立打开同一文件实现共享操作,我们在第1章讲过,不过那时涉及到的多个进程是不相干进程,而现在我们这里要讲
的例子,里面所涉及到的两个进程是父子关系,不过情况是一样的。

1)代码演示

2)文件表结构
图;

独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表。

因为拥有不同的文件表,所以他们拥有各自独立的文件读写位置,会出现相互覆盖情况,如果不想相互覆盖,
需要加O_APPEND标志。

(2)情况2:fork之前打开文件

1)代码演示

2)文件表结构
图:

子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件。
像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”。

由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。

子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的,同样的父进程的
0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的,我们前面介绍过,最原始的进
程是init进程。

init进程会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,之后所有进程的0 1 2,
实际上都是从最开始的init进程那里继承而来的。

init 012 012 012 012 012 012
原始进程————>进程————————>进程———>...———>终端进程——————>a.out进程——————>a.out进程
| | |
| | |
V V V
进程012 进程012 进程012
| | |
| | |
... ... ...

2.4 子进程会继承父进程的哪些属性

2.4.1 子进程继承如下性质

(1)用户ID,用户组ID
(2)进程组ID(下一篇讲)
(3)会话期ID(下一篇讲)
(4)控制终端(下一篇讲)
(5)当前工作目录
(6)根目录
(7)文件创建方式屏蔽字
(8)环境变量
(9)打开的文件描述符
等等

2.4.2 子进程独立的属性
(1)进程ID。
(2)不同的父进程ID。
(3)父进程设置的锁,子进程不能被继承。
等等

3. exec加载器

exec加载器就是我们之前介绍的加载函数。

3.1 exec的作用

父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有太大的意义,我们需要在子进
程空间里面运行全新的代码,这样才有意义。

怎么运行新代码?
我们可以在if(ret==0){}里面直接写新代码,但是这样子很麻烦,如果新代码有上万行甚至更多的话,这种做法显然是不行的,因此
就有了exec加载器。

有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可。

3.2 exec函数族

exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载函数。
其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封装得到的库函数,因此我们这里重点介绍
execve函数,这个函数懂了,其它的函数原理是一样的。

3.2.1 execve函数原型

#include <unistd.h>

int execve(const char *filename, char **const argv, char **const envp);

(1)功能:向子进程空间加载新程序代码(编译后的机器指令)。

(2)参数:
1)filename:新程序(可执行文件)所在的路径名

可以是任何编译型语言所写的程序,比如可以是c、c++、汇编等,这些语言所写的程序被编译为机器指令后,
都可以被execve这函数加载执行。

正是由于这一点特性,我们才能够在C语言所实现的OS上,运行任何一种编译型语言所编写的程序。

疑问:java可以吗?
java属于解释性语言,它所写的程序被编译后只是字节码,并不是能被CPU直接执行的机器指令,所以不能被execve直接加
载执行,而是被虚拟机解释执行。

execve需要先加载运行java虚拟机程序,然后再由虚拟机程序去将字节码解释为机器指令,再有cpu去执行,在后面还会详细
讨论这个问题。

2)argv:传给main函数的参数,比如我可以将命令行参数传过去
3)envp:环境变量表

(3)返回值:函数调用成功不返回,失败则返回-1,且errno被设置。

(4)代码演示

命令行参数/环境表 命令行参数/环境表 命令行参数/环境表
终端窗口进程——————————————————>a.out(父进程)——————————————————————>a.out(子进程)——————————————>新程序
fork exec

exec的作用:将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和数据,让子进程空间运行全
新的程序。

3.3 在命令行执行./a.out,程序是如何运行起来的
(1)窗口进程先fork出子进程空间
(2)调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参

3.4 双击快捷图标,程序是怎么运行起来的
(1)图形界面进程fork出子进程空间
(2)调用exec函数,加载快捷图标所指向程序的代码
以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表。

4. system函数

如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是这样的操作很麻烦,
所以就有了system这个库函数,这函数封装了fork和execve函数,调用时会自动的创建子进程空间,并把新程序的代码加载到
子进程空间中,然后运行起来。

虽然有system这函数,但是我们还是单独的介绍了fork和execve函数,因为希望通过这两个函数的介绍,让大家理解当有OS支持时,
程序时如何运行起来的。

4.1 system函数原型
#include <stdlib.h>

int system(const char *command);

(1)功能:创建子进程,并加载新程序到子进程空间,运行起来。

(2)参数:新程序的路径名

(3)代码演示

system(“ls”);

system(“ls -al”);

5. 回收进程资源

进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。

5.1 为什么要回收进程的资源?

(1)程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让出来(释放)。

(2)OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结构体所占用的内存空间也
需要被释放。

(3)等其它资源

5.2 由谁来回收进程资源
由父进程来回收,父进程运行结束时,会负责释放子进程资源。

5.3 僵尸进程和孤儿进程

5.3.1 僵尸进程

子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。

为什么子进程会变成僵尸进程?
子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源不拉屎僵尸进程。

就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸。

5.3.2 孤儿进程
没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。

为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程,每当被托管的子进程终止时,init会立即
主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵尸进程的机会。

5.3.3 演示
(1)僵尸进程

ps查看到的进程状态
R 正在运行
S 处于休眠状态
Z 僵尸进程,进程运行完了,等待被回收资源。

(2)孤儿进程

佳嵌工作室

原文地址:https://www.cnblogs.com/lemaden/p/10422354.html

时间: 2024-10-17 03:29:28

进程控制(粗略概括)的相关文章

Linux进程控制知识总结

目录 一:进程标识符(ID) 二:进程操作 2.1创建一个进程 2.2 fork函数出错情况 2.3创建一个共享空间的子进程 2.4退出程序 2.5设置进程所有者 三:执行程序 3.1 exec函数 3.2 执行解释器文件 3.3在程序中执行Shell命令 四:关系操作符 4.1等待进程退出 4.2 等待指定的进程 进程控制 -- 一步 一:进程标识符(ID) 进程ID是用来标识进程的编号,就像身份证一样.不同的进程有不同的ID,可以通过ID来查询进程.进程标识符的类型是pit_t,其本质是一个

Linux - 进程控制 代码(C)

进程控制 代码(C) 本文地址:http://blog.csdn.net/caroline_wendy 输出进程ID,getpid(). 代码: /*By C.L.Wang * Eclipse CDT * Ubuntu 12.04 * 2014.10.5*/ #include "apue.h" #include "error.h" int main(void) { printf("hello world from process ID %ld\n"

六、Linux进程控制

1. Linux进程概述 进程是一个程序一次执行的过程,它和程序有本质区别. 程序是静态的,它是一些保存在磁盘上的指令的有序集合:而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建.调度和消亡的过程,是Linux的基本调度单位. 那么从系统的角度看如何描述并表示它的变化呢?在这里,是通过进程控制块(PCB)来描述的.进程控制块包含了进程的描述信息.控制信息以及资源信息,它是进程的一个静态描述. 内核使用进程来控制对CPU和其他系统资源的访问,并且使用进程来决定在CPU上运行哪个程

Linux进程控制编程

一.获取ID #include<sys/types.h> #include<unistd.h> pid_t getpid(void)    获取本进程ID pid_t getppid(void)  获取父进程ID 父进程:现有进程中,创建新的进程. 例:getpid.c #include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { printf("PID=%d\

APUE(8)---进程控制(1)

一.进程标识 每个进程都有一个非负整型标识的唯一进程ID.因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性.进程ID虽然是唯一的, 但是却是可以复用的.ID为0的进程通常是调度进程,常常被称为交换进程(swapper).该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程.进程ID为1通常是init进程,在自举过程结束时由内核调用.此进程负责在自举内核后启动一个UNIX系统,init通常读取与系统有关的初始化文件,并将系统引导一个状态.init进程绝不会

Linux进程控制(二)

1. 进程的创建 Linux下有四类创建子进程的函数:system(),fork(),exec*(),popen() 1.1. system函数 原型: #include <stdlib.h> int system(const char *string); system函数通过调用shell程序/bin/sh –c来执行string所指定的命令,该函数在内部是通过调用execve("/bin/sh",..)函数来实现的.通过system创建子进程后,原进程和子进程各自运行,

linux 命令及进程控制

main.c  main.o/main.obj  main/main.exe          编译                连接 程序运行;      两步: gcc/g++  -c  main.c/main.cpp  -> main.o              gcc/g++ -o main  main.o -> main(可执行文件)     一步:  gcc -o main mian.c  -> main    工程文件:       rm  *.o     gcc  -

Linux进程控制——exec函数族

原文:http://www.cnblogs.com/hnrainll/archive/2011/07/23/2114854.html 1.简介 在Linux中,并不存在exec()函数,exec指的是一组函数,一共有6个,分别是: #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char

APUE学习笔记(第八章 进程控制)

本章介绍UNIX系统的进程控制,包括创建新进程.执行程序和进程终止. 进程标识 每一个进程都有一个非负整数表示的唯一进程ID,除了进程ID,每个进程还有一些其他标识符.下列函数返回这些标识符 #include <unistd.h> pid_t getpid(void); pid_t getppid(void); uid_t getuid(void); uid_t geteuid(void); gid_t getgid(void); gid_t getegid(void); 函数fork 一个现

第八章:进程控制

8.1:引言 本章介绍Unix的进程控制,包括创建新进程.执行程序和进程终止.还将说明进程属性的各种ID--实际.有效和保存的用户和组ID,以及它们如何受到进程控制原语的影响.还包括解释器文件和system函数,最后讲述大多数Unix系统所提供的进程会计机制. 8.2:进程标识符 每个进程都有一个非负整型表示的唯一进程ID.虽然是唯一的,但是进程ID却可以重用,当一个进程终止后,其进程ID就可以再次使用了.Unix使用延迟重用算法,避免新进程的ID等于最近终止的进程的ID. 除了进程ID,每个进