【linux】进程的概念与控制详细讲解(冯诺依曼体系、僵尸进程、写时拷贝....

目录

冯诺依曼体系结构

操作系统(Operator System)

进程 

基本概念

组织进程

查看进程

进程状态

 僵尸进程危害

环境变量

程序地址空间

挂起

进程创建

写时拷贝

进程终止

_exit函数

exit函数

参数:


冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成

关于冯诺依曼,必须强调几点:

这里的存储器指的是内存不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。一句话,所有设备都只能直接和内存打交道。

操作系统(Operator System)

概念
任何计算机系统都包含一个基本的程序集合,称为操作系统 (OS) 。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)其他程序(例如函数库, shell 程序等等)
设计 OS 的目的
与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境
定位
在整个计算机软硬件架构中,操作系统的定位是: 一款纯正的 搞管理 的软件
如何理解 " 管理 "
总结
计算机管理硬件
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

进程 

基本概念

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源( CPU 时间,内存)的实体。
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为 PCB process control block ), Linux 操作系统下的 PCB : task_struct
task_struct-PCB 的一种
Linux 中描述进程的结构体叫做 task_struct。task_struct Linux 内核的一种数据结构,它会被装载到 RAM( 内存 ) 里并且包含着进程的信息。
task_ struct 内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据 : 进程执行时处理器的寄存器中的数据 [ 休学例子,要加图 CPU ,寄存器 ]
I O 状态信息 : 包括显示的 I/O 请求 , 分配给进程的 I O 设备和被进程使用的文件列表。

组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。

查看进程

进程的信息可以通过 /proc 系统文件夹查看
如:要获取 PID 1 的进程信息,你需要查看 /proc/1 这个文件夹。

大多数进程信息同样可以使用topps这些用户级工具来获取 

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 while(1){
 sleep(1);
 }
 return 0;
}

通过系统调用获取进程标示符
进程 id PID
父进程 id PPID
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 printf("pid: %d\n", getpid());
 printf("ppid: %d\n", getppid());
 return 0;
}
fork 之后通常要用 if 进行分流
  1. 父进程调用fork,返回子线程pid(>0)

  2. 子进程调用fork,子进程返回0,调用失败的话就返回-1

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 int ret = fork();
 if(ret < 0){
 perror("fork");
 return 1;
 }
 else if(ret == 0){ //child
 printf("I am child : %d!, ret: %d\n", getpid(), ret);
 }else{ //father
 printf("I am father : %d!, ret: %d\n", getpid(), ret);
 }
 sleep(1);
 return 0;
}

进程状态

看看 Linux 内核源代码怎么说
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux 内核里,进程有时候也叫做任务)。
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

进程状态查看
ps aux / ps axj 命令

Z(zombie)- 僵尸进程
僵死状态( Zombies )是一个比较特殊的状态。当进程退出并且父进程(使用 wait() 系统调用
没有读取到子进程退出的返回代码时就会产生僵死 ( ) 进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态

#include <stdio.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id < 0){
 perror("fork");
 return 1; }
 else if(id > 0){ //parent
 printf("parent[%d] is sleeping...\n", getpid());
 sleep(30);
 }else{
 printf("child[%d] is begin Z...\n", getpid());
 sleep(5);
 exit(EXIT_SUCCESS);
 }
 return 0;
}

 僵尸进程危害

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!内存泄漏?是的!
孤儿进程
父进程如果提前退出,那么子进程后退出,进入 Z 之后,那该如何处理呢?
父进程先退出,子进程就称之为 孤儿进程
孤儿进程被 1 init 进程领养,当然要有 init 进程回收喽。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id < 0){
 perror("fork");
 return 1;
 }
 else if(id == 0){//child
 printf("I am child, pid : %d\n", getpid());
 sleep(10);
 }else{//parent
 printf("I am parent, pid: %d\n", getpid());
 sleep(3);
 exit(0);
 }
 return 0;
}

进程优先级
基本概念
cpu 资源分配的先后顺序,就是指进程的优先权( priority )。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux很有用,可以改善系统性能。 还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能。
查看系统进程
linux 或者 unix 系统中,用 ps –l 命令则会类似输出以下几个内容
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
PRI 也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高那 NI ? 就是我们所要说的 nice值了,其表示进程可被执行的优先级的修正数值PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为: PRI(new)=PRI(old)+nice
这样,当 nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在 Linux 下,就是调整进程 nice值nice 其取值范围是 -20 19 ,一共 40 个级别。
PRI vs NI
需要强调一点的是,进程的 nice 值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解 nice 值是进程优先级的修正修正数据

环境变量

基本概念
环境变量 (environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写 C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )
SHELL : 当前 Shell, 它的值通常是 /bin/bash
查看环境变量方法
echo $NAME //NAME: 你的环境变量名称

程序地址空间

研究背景
kernel 2.6.32
32 位平台
程序地址空间回顾

 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
 pid_t id = fork();
 if(id < 0){
 perror("fork");
 return 0;
 }
 else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }else{ //parent
 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }
 sleep(1);
 return 0;
}

 输出

// 与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
我们发现,输出出来的变量值和地址是一模一样的,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动 :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
 pid_t id = fork();
 if(id < 0){
 perror("fork");
 return 0;
 }
 else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
 g_val=100;
 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }else{ //parent
 sleep(3);
 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }
 sleep(1);
 return 0;
}
输出结果
// 与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
变量内容不一样 , 所以父子进程输出的变量绝对不是同一个变量

但地址值是一样的,说明,该地址绝对不是物理地址!
Linux 地址下,这种地址叫做 虚拟地址
我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS 统一管理
OS 必须负责将 虚拟地址 转化成 物理地址
进程地址空间
所以之前说 程序的地址空间 是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:

进程地址空间

  • 使用物理地址不安全,所以使用虚拟地址
  • 每一个进程有自己的pcb
  • 操作系统给每一个进程创造虚拟地址空间

深入理解虚拟地址

  • 虚拟地址会通过映射机制来访问实际的物理内存物理地址(页表)
  • 地址空间不要仅仅理解为是os内部要遵守的,其实编译器也要遵守,即编译器编译代码的时候,就已经给我们形成了,各个区域,并且采用和linux内核一样的编址方式,给每一个变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段早已经具有了一个虚拟地址
  • Cpu拿到的都是虚拟地址,通过页表跳转读取到物理地址的内容
  • 本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,而并不是物理内存
  • 只有当你真正的通过虚拟地址传递到cpu再访问物理地址空间的时候(是由操作系统自动完成,用户包括进程完全没有感知),才会执行内存相关的管理算法,帮你申请内存,构建页表映射关系,然后在进行内存的访问

因为在物理内存中理论上可以任意位置加载,那么是不是物理内存的所有数据和代码都是乱序的?

没错,但是因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,

那么是不是在进程视角所有内存分布都是有序的?

地址空间+页表的存在,可以将内存分布有序化。

进程要访问的物理内存中的数据和代码,可能并没有在物理内存中,同样也可以让不同的进程映射到不同的物理内存,很容易让进程独立性的实现

进程的独立性可以通过地址空间+页表的方式实现。

因为有地址空间的存在,每一个进程都认为自己拥有4gb的空间,并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。

所以每一个进程是不需要知道其他进程的存在的。·

加载本质是创建进程,那么是不是必须非要把所有程序的代码和数据加载到内存中,并且创建内核数据结构建立映射关系?

在最极端的情况下,甚至只有内核结构被创建出来了。

理论上可以实现对程序的分批加载

挂起

进程的数据和代码被换出了,就叫被挂起

创建子进程,不需要将不会被访问的或者只会读取的数据拷贝一份

只有将来会被父或者子进程写入的数据,值得被拷贝。一般来说即便是os,也无法提前知道哪些空间可能会被写入,所以os选择写时拷贝技术,来进行将父子进程的数据分离。

进程终止时,操作系统做了什么

要释放进程申请的相关内核数据结构和对应的数据和代码。本质就是释放系统资源


进程创建

fork 函数初识
linux fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork 返回,开始调度器调度

 

当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序
int main( void )
{
 pid_t pid;
 printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
 printf("After:pid is %d, fork return %d\n", getpid(), pid);
 sleep(1);
 return 0;
} 
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行 before ,两行 after 。进程 43676 先打印 before 消息,然后它又打印 after 。另一个 after
消息有 43677 打印的。注意到进程 43677 没有打印before,为什么呢?如下图所示

所以, fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器 决定。
fork 函数返回值
子进程返回0,
父进程返回的是子进程的pid 

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图
fork 常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork 调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制

进程终止

进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. main 返回
2. 调用 exit
3. _exit
异常退出:
ctrl + c ,信号终止

_exit函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然 status int ,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 $?发现返回值是 255

exit函数

#include <unistd.h>
void exit(int status);

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

1. 执行用户通过 atexit on_exit 定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit  
int main()
{
 printf("hello");
 exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
 printf("hello");
 _exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#

 return退出

return 是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main的返回值当做 exit 的参数。

进程等待

进程等待必要性
  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,杀人不眨眼kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
  • 成功返回被等待进程pid,失败返回-1
参数:
输出型参数,获取子进程退出状态 , 不关心则可以设置成为NULL        
waitpid 方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

pid
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果子进程已经退出,调用 wait/waitpid 时, wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用 wait/waitpid ,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

获取子进程 status
wait waitpid ,都有一个 status参数,该参数是一 个输出型参数,由操作系统填充。
如果传递 NULL ,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status低16比特位):
测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main( void )
{
 pid_t pid;
 if ( (pid=fork()) == -1 )
 perror("fork"),exit(1);
 if ( pid == 0 ){
 sleep(20);
 exit(10);
 } else {
 int st;
 int ret = wait(&st);
 
 if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
 printf("child exit code:%d\n", (st>>8)&0XFF);
 } else if( ret > 0 ) { // 异常退出
 printf("sig code : %d\n", st&0X7F );
 }
 }
}
测试结果:
 [root@localhost linux]# ./a.out #等20秒退出
 child exit code:10 
 [root@localhost linux]# ./a.out #在其他终端kill掉
 sig code : 9

 具体代码实现

进程的阻塞等待方式
int main()
{
 pid_t pid;
 pid = fork();
 if(pid < 0){
 printf("%s fork error\n",__FUNCTION__);
 return 1;
 } else if( pid == 0 ){ //child
 printf("child is run, pid is : %d\n",getpid());
 sleep(5);
 exit(257);
 } else{
 int status = 0;
 pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
 printf("this is test for wait\n");
 if( WIFEXITED(status) && ret == pid ){
 printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 }else{
 printf("wait child failed, return.\n");
 return 1;
 }
}
 return 0;
}
运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
进程的非阻塞等待方式:
#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
 pid_t pid;
 
 pid = fork();
 if(pid < 0){
 printf("%s fork error\n",__FUNCTION__);
 return 1;
 }else if( pid == 0 ){ //child
 printf("child is run, pid is : %d\n",getpid());
 sleep(5);
 exit(1);
 } else{
 int status = 0;
 pid_t ret = 0;
 do
 {
 ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
 if( ret == 0 ){
 printf("child is running\n");
 }
 sleep(1);
 }while(ret == 0);
 
 if( WIFEXITED(status) && ret == pid ){
 printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 }else{
 printf("wait child failed, return.\n");
 return 1;
 }
 }
 return 0;
}
进程程序替换
替换原理
fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数
以执行另一个程序。当进程调用一种 exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动
例程开始执行。调用 exec 并不创建新进程 , 所以调用 exec 前后该进程的id并未改变

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小刘好好学习

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

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

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

打赏作者

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

抵扣说明:

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

余额充值