注:本文部分图片来源于网络,如有侵权,请告知删除

1. 什么是进程?

在了解进程概念之前,我们需要先知道程序的概念。

程序,是指编译好的二进制文件,这些文件在磁盘上,并不占用系统资源。

进程,指的是一个程序的执行实例,是操作系统分配系统资源的单位,这里的系统资源有CPU时间,内存等。当程序运行起来,产生一个进程。

也就是说,相比于程序,进程是一个动态的概念。

2. 用什么来描述进程?

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。教材中称为PCB(process control block),不同的操作系统下有不同的PCB,Linux 下的进程控制块是 task_struct。

task_struct是Linux内核的一种数据结构,当一个进程创建时,系统会先将程序加载到内存,同时会将task_struct装载到内存中,在task_struct中包含着进程的信息。

task_struct的内容主要分为以下几类:

  • 标示符(PID) : 描述本进程的唯一标示符,用来区别其他进程,本质上是一个非负整数。

  • 进程状态: 任务状态,退出代码,退出信号等。

  • 上下文数据: 进程执行时处理器的寄存器中的数据。

  • 程序计数器: 程序中即将被执行的下一条指令的地址。

  • 文件描述符表,包含很多指向 file 结构体的指针。

  • 优先级: 相对于其他进程的优先级。

  • 其他信息。

3. PID、PPID

为了便于管理,操作系统中有父子进程的概念。子进程会继承父进程的属性和权限,而父进程也可以系统地管理子进程。

进程的标志符是PID,是进程的唯一标识,而父进程的标志符是PPID。

要查看进程的父子关系,可以用命令ps axj

我们在后台运行一个./test可执行文件,用如下命令查看该进程的父子信息

可以看到,该进程的进程PID为7711,其父进程PPID为29455

要获取进程id和父进程id,可以使用getpid()和getppid()函数:

获取当前进程 ID pid_t getpid(void);

获取当前进程的父进程 ID pid_t getppid(void);

如运行如下代码后,可以输出该进程的id和父进程id

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}

输出结果:

4. fork函数

运行man 2 fork后,可以看到pid_t fork(void);

fork函数是用于创建子进程的一个函数,当父进程调用fork函数后,会创建一个子进程,父子进程代码共享,数据各自开辟空间。

一般情况下,fork之后通常要进行分流,如代码1

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int g_val = 0; int main()
{
pid_t id = fork();
if(id < 0){
perror("fork fail");
return 1;
}
else if(id == 0) {
//child
printf("g_val = %d,child_pid = %d , &g_val = %p\n",g_val,getpid(),&g_val);
}
else {
//parent
printf("g_val = %d,parent_pid = %d , &g_val = %p\n",g_val,getpid(),&g_val);
}
return 0;
}

执行结果如下

可以看出,分流之后,父进程执行的是id>0的代码,而子进程执行的是id == 0 的代码,也就是说,fork是有两个返回值的,如果子进程创建成功,fork给父进程返回的是子进程的PID,给子进程返回0。

需要注意的是,子进程执行的是fork之后的代码。这是为什么?

在父进程创建好子进程后,父子进程代码共有,父进程会将自己的数据拷贝给子进程,其中就包括了父进程程序计数器的值。程序计数器内存放的是程序中即将被执行的下一条指令的地址,由于父进程已经执行了fork前面的代码,因此子进程会和父进程一样,都执行fork之后的代码。

5. 进程的状态

当一个进程实体从磁盘加载到内存时,会创建对应的task_stuct,进程有不同的状态。在Linux中,所有运行在系统里的进程都以task_struct链表的形式存在内核里,根据状态的不同,可以将

task_struct中有关于进程状态的描述:

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状态:可执行状态,只有该状态的进程才可以上处理机运行。同一时刻可以有多个进程同时处于R状态,除了上处理机的进程外,其余R状态的进程以链表的形式组成队列,等待上处理机。在操作系统教材中的运行态和就绪态,在Linux中统一为R状态。

S状态:可中断睡眠状态,进程因为等待某些资源,而没有上处理机运行,该状态即S状态。当得到等待的资源,或者接收到某些异步信号时,进程将会被唤醒。一般情况下用ps命令查看进程状态,大多数进程都是S状态。

D状态:深度睡眠状态,该状态下不接受一些异步信号。该状态存在的原因是操作系统的某一些操作要求是原子操作,中间不可以接受其他异步信号的干扰,只要对应资源不得到满足,就一直处于D状态。例如, kill -9 也杀不死D状态的进程。而实际中,我们用ps命令几乎是无法捕捉到D状态的进程,因为原子操作往往比较短暂。

T状态:可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。

X状态:死亡状态,该状态是返回状态,在任务列表中看不到。

Z状态:僵尸状态,该状态是一个特殊的状态。当进程退出时,如果父进程没有读取到子进程退出的返回代码,就会产生僵尸进程。僵尸进程会一直以Z状态留在进程表中,等待父进程读取其退出状态。即便是退出状态的进程,本身也需要用PCB进行维护,也就是说,如果父进程不读取子进程的退出信息,子进程的PCB会一直在内存中,从而造成了内存泄漏。

除了僵尸进程,系统中还可能存在另外一种进程——孤儿进程。当父进程先退出时,子进程就成了孤儿进程,此时孤儿进程会被1号init进程领养,其PPID变为1。

6. 进程地址空间

我们将第4节讲解fork函数时的代码稍作修改

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int g_val = 0; int main()
{
pid_t id = fork();
if(id < 0){
perror("fork fail");
return 1;
}
else if(id == 0) {
//child
g_val = 10000;
printf("g_val = %d,child_pid = %d , &g_val = %p\n",g_val,getpid(),&g_val);
}
else {
//parent
sleep(3);//这段代码让父进程休眠3s,保证子进程的代码先执行,让子进程修改g_val
printf("g_val = %d,parent_pid = %d , &g_val = %p\n",g_val,getpid(),&g_val);
}
return 0;
}

执行结果如下

我们惊奇地发现,父进程和子进程的&g_val是一样的,但是g_val居然不一样!

我们知道,相同的物理内存单元中不可能存储不同的两个数,也就是说,这里的地址并不是实际的物理地址,而是虚拟地址。那么,操作系统是如何管理进程的地址空间呢?

6.1 mm_struct

对于操作系统而言,管理的方式是先用数据结构进行描述,再将数据结构进行组织。我们知道当一个进程创建时,会创建对应的PCB,在Linux中,task_struct中有一个结构体——struct mm_struct,这个结构体就是用来描述该进程虚拟地址的结构体。

mm_struct源码如下

struct mm_struct {

    //指向线性区对象的链表头
struct vm_area_struct * mmap; /* list of VMAs */
//指向线性区对象的红黑树
struct rb_root mm_rb;
//指向最近找到的虚拟区间
struct vm_area_struct * mmap_cache; /* last find_vma result */ //用来在进程地址空间中搜索有效的进程地址空间的函数
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags); unsigned long (*get_unmapped_exec_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags); //释放线性区时调用的方法,
void (*unmap_area) (struct mm_struct *mm, unsigned long addr); //标识第一个分配文件内存映射的线性地址
unsigned long mmap_base; /* base of mmap area */ unsigned long task_size; /* size of task vm space */
/*
* RHEL6 special for bug 790921: this same variable can mean
* two different things. If sysctl_unmap_area_factor is zero,
* this means the largest hole below free_area_cache. If the
* sysctl is set to a positive value, this variable is used
* to count how much memory has been munmapped from this process
* since the last time free_area_cache was reset back to mmap_base.
* This is ugly, but necessary to preserve kABI.
*/
unsigned long cached_hole_size; //内核进程搜索进程地址空间中线性地址的空间空间
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ //指向页表的目录
pgd_t * pgd; //共享进程时的个数
atomic_t mm_users; /* How many users with user space? */ //内存描述符的主使用计数器,采用引用计数的原理,当为0时代表无用户再次使用
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ //线性区的个数
int map_count; /* number of VMAs */ struct rw_semaphore mmap_sem; //保护任务页表和引用计数的锁
spinlock_t page_table_lock; /* Protects page tables and some counters */ //mm_struct结构,第一个成员就是初始化的mm_struct结构,
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/ /* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/ mm_counter_t _file_rss;
mm_counter_t _anon_rss;
mm_counter_t _swap_usage; //进程拥有的最大页表数目
unsigned long hiwater_rss; /* High-watermark of RSS usage */、
//进程线性区的最大页表数目
unsigned long hiwater_vm; /* High-water virtual memory usage */ //进程地址空间的大小,锁住无法换页的个数,共享文件内存映射的页数,可执行内存映射中的页数
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
//用户态堆栈的页数,
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
//维护代码段和数据段
unsigned long start_code, end_code, start_data, end_data;
//维护堆和栈
unsigned long start_brk, brk, start_stack;
//维护命令行参数,命令行参数的起始地址和最后地址,以及环境变量的起始地址和最后地址
unsigned long arg_start, arg_end, env_start, env_end; unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ struct linux_binfmt *binfmt; cpumask_t cpu_vm_mask; /* Architecture-specific MM context */
mm_context_t context; /* Swap token stuff */
/*
* Last value of global fault stamp as seen by this process.
* In other words, this value gives an indication of how long
* it has been since this task got the token.
* Look at mm/thrash.c
*/
unsigned int faultstamp;
unsigned int token_priority;
unsigned int last_interval; //线性区的默认访问标志
unsigned long flags; /* Must use atomic bitops to access the bits */ struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
spinlock_t ioctx_lock;
struct hlist_head ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
/*
* "owner" points to a task that is regarded as the canonical
* user/owner of this mm. All of the following must be true in
* order for it to be changed:
*
* current == mm->owner
* current->mm != mm
* new_owner->mm == mm
* new_owner->alloc_lock is held
*/
struct task_struct *owner;
#endif #ifdef CONFIG_PROC_FS
/* store ref to file /proc/<pid>/exe symlink points to */
struct file *exe_file;
unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
/* reserved for Red Hat */
#ifdef __GENKSYMS__
unsigned long rh_reserved[2];
#else
/* How many tasks sharing this mm are OOM_DISABLE */
union {
unsigned long rh_reserved_aux;
atomic_t oom_disable_count;
}; /* base of lib map area (ASCII armour) */
unsigned long shlib_base;
#endif
};

因此,进程地址空间实际上就是结构体mm_struct所描述的虚拟空间,每个进程都有自己的虚拟地址空间。每个进程的虚拟地址如下图所示。

在Linux中,采用分页存储的方式对内存进行管理。既然我们平时所看到的地址不是实际的物理地址,那就需要操作系统将虚拟地址映射为物理地址。操作系统是借助页表来实现虚拟地址和物理地址的映射的,页表的本质也是一个数据结构,最主要的两项就是进程的虚拟地址和实际物理地址的映射关系。

6.2 写时拷贝

在我们的代码中,当fork创建子进程时,会将父进程的mm_struct也拷贝给子进程,一开始,内存中只有一份g_val,当子进程修改g_val时,由于父子进程的数据是各自私有的,进程之间的执行应该具有独立性,因此子进程修改g_val不应该影响到父进程。此时就会发生写时拷贝,即子进程在内存中开辟一块新的空间,将修改后的值填入该空间,并且修改子进程页表中虚拟地址映射的实际物理地址。

因此,我们看到了上述相同虚拟地址中存储的数值不同的场景。

6.3 为什么要有进程地址空间?

这是因为引入了进程地址空间后,可以保证每个进程所用的空间独立而连续3。一个进程的越界操作并不会影响另一个进程,这样就实现了内存的保护。同时,每个进程地址空间是远大于实际内存空间的,这样也可以通过虚拟的方式实现内存的扩充。当一个进程退出后,我们只需要清除掉该进程的mm_struct和页表就可,有利于内存的分配回收。

Linux系统编程之进程概念的更多相关文章

  1. linux系统编程之进程(一)

    今天起,开始学习linux系统编程中的另一个新的知识点----进程,在学习进程之前,有很多关于进程的概念需要了解,但是,概念是很枯燥的,也是让人很容易迷糊的,所以,先抛开这些抽象的概念,以实际编码来熟 ...

  2. Linux系统编程——Daemon进程

    目录 Daemon进程介绍 前提知识 Daemon进程的编程规则 Daemon进程介绍 Daemon运行在后台也称作"后台服务进程". 它是没有控制终端与之相连的进程.它独立与控制 ...

  3. linux系统编程之进程(五)

    今天继续学习系统编程,学习的主题还是进程,今天主要讨论的是守护进程相关的概念,开始进入正题: 什么是守护进程:       守护进程的创建步骤: 在描述它之前,首先得先了解两个概念:进程组.会话期: ...

  4. Linux系统编程 —线程同步概念

    同步概念 同步,指对在一个系统中所发生的事件之间进行协调,在时间上出现一致性与统一化的现象. 但是,对于不同行业,对于同步的理解略有不同.比如:设备同步,是指在两个设备之间规定一个共同的时间参考:数据 ...

  5. linux系统编程之进程(八):守护进程详解及创建,daemon()使用

    一,守护进程概述 Linux Daemon(守护进程)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.它不需要用户输入就能运行而且提供某种服务,不是对整个 ...

  6. linux系统编程之进程(六):父进程查询子进程的退出,wait,waitpid

    本节目标: 僵进程 SIGCHLD wait waitpid 一,僵尸进程 当一个子进程先于父进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行,或者父进程调用了wait才告终止. ...

  7. linux系统编程--守护进程,会话,进程组,终端

    终端: 在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal), 进程中,控制终端是保存在PCB中的信息,而f ...

  8. linux系统编程之进程(二):进程生命周期与PCB(进程控制块)

    本节目标: 进程状态变迁 进程控制块 进程创建 进程撤消 终止进程的五种方法 一,进程状态变迁 进程的三种基本状态 就绪(Ready)状态 当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便 ...

  9. linux系统编程之进程(三):进程复制fork,孤儿进程,僵尸进程

    本节目标: 复制进程映像 fork系统调用 孤儿进程.僵尸进程 写时复制 一,进程复制(或产生)      使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文.进程堆栈. ...

随机推荐

  1. jQuery <= 1.11.3 DomXSS漏洞

    听团里说WordPress又爆跨站漏洞了:" XSS漏洞在Jetpack和二十五默认主题影响百万WordPress用户 ",分析发现原来是jQuery老版本的DOM XSS漏洞[错 ...

  2. 网站URL Rewrite(伪静态)设置方法

    1.如果您的服务器支持.htaccess,则无需设置,网站根目录下的.htaccess已经设置好规则.规则详情:http://download.destoon.com/rewrite/htaccess ...

  3. Java基础系列(7)- 标识符和关键字

    关键字 标识符 Java所有的组成部分都需要名字.类名.变量名.方法名都称为标识符 首字母以字母(A-Z或者a-z),美元符号($),或者下划线(_)开头 首字母之后可以用字母.美元符号.下划线.数字 ...

  4. centos7 设置dns

    查看当前网络连接 nmcli connection show NAME UUID TYPE DEVICE eth0 5fb06bd0-0bb0-7ffb-45f1-d6edd65f3e03 802-3 ...

  5. P3964-[TJOI2013]松鼠聚会【计算几何】

    正题 题目链接:https://www.luogu.com.cn/problem/P3964 题目大意 给出\(n\)个点,求一个点使得它到所有点的切比雪夫距离和最小. \(0\leq n\leq 1 ...

  6. P5445-[APIO2019]路灯【set,树状数组套线段树】

    正题 题目链接:https://www.luogu.com.cn/problem/P5445 题目大意 \(n+1\)个点,\(i\)和\(i+1\)个点之间有一条边,\(q\)个操作 断开/连接第\ ...

  7. Skywalking-12:Skywalking SPI机制

    SPI机制 基本概述 SPI 全称 Service Provider Interface ,是一种服务发现机制.通过提供接口.预定义的加载器( Loader )以及约定俗称的配置(一般在 META-I ...

  8. Django整理(四) - URL配置

    1. URL配置 一.需求 1. 需求:在浏览器访问URL地址 http://127.0.0.1:8000/users/index 时,显示hello django信息 2. 实现 i. 需要编写一个 ...

  9. MYSQL小版本升级(5.7.21至5.7.25)

    1.环境确认 [root@mysql ~]# ps -ef |grep -i mysql root 9173 1 0 2020 ? 00:00:00 /bin/sh /mysql/data/mysql ...

  10. Jave Hbase AP

    Hbase API 类和数据模型的对应关系 HBaseAdmin 类:org.apache.hadoop.hbase.client.HBaseAdmin 作用:提供了一个接口来管理 HBase 数据库 ...