注:本文中的部分图片来自互联网。如果有侵权行为,请通知我们删除。
[En]
Note: some of the pictures in this article come from the Internet. If there is any infringement, please inform us to delete it.
1. 什么是进程?
在我们理解过程的概念之前,我们需要了解程序的概念。
[En]
Before we can understand the concept of process, we need to know the concept of program.
程序指的是位于磁盘上且不占用系统资源的已编译二进制文件。
[En]
A program refers to compiled binary files that are on disk and do not take up system resources.
进程,指的是一个程序的执行实例,是操作系统分配系统资源的单位,这里的系统资源有CPU时间,内存等。当程序运行起来,产生一个进程。
换句话说,与程序相比,过程是一个动态的概念。
[En]
In other words, compared to programs, process is a dynamic concept.
2. 用什么来描述进程?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。教材中称为PCB(process control block),不同的操作系统下有不同的PCB,Linux 下的进程控制块是 task_struct。
task_struct是Linux内核的一种数据结构,当一个进程创建时,系统会先将程序加载到内存,同时会将task_struct装载到内存中,在task_struct中包含着进程的信息。
task_struct的内容主要分为以下几类:
- 标示符(PID) : 描述本进程的唯一标示符,用来区别其他进程,本质上是一个非负整数。
- 进程状态: 任务状态,退出代码,退出信号等。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 文件描述符表,包含很多指向 file 结构体的指针。
- 优先级: 相对于其他进程的优先级。
- 其他信息。
3. PID、PPID
为了便于管理,在操作系统中引入了父子进程的概念。子流程继承父流程的属性和权限,父流程也可以对子流程进行系统管理。
[En]
In order to facilitate management, there is the concept of parent-child process in the operating system. The child process inherits the properties and permissions of the parent process, and the parent process can also systematically manage the child process.
进程的标志符是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;
}
</unistd.h></sys></stdio.h>
输出结果:
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;
}
</unistd.h></sys></stdio.h>
执行结果如下
可以看出,分流之后,父进程执行的是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;
}
</unistd.h></sys></stdio.h>
执行结果如下
我们惊奇地发现,父进程和子进程的&g_val是一样的,但是g_val居然不一样!
我们知道在同一个物理存储单元中存储两个不同的数字是不可能的,也就是说,这里的地址不是实际的物理地址,而是虚拟地址。那么,操作系统如何管理进程的地址空间呢?
[En]
We know that it is impossible to store two different numbers in the same physical memory unit, that is, the address here is not the actual physical address, but the virtual address. So how does the operating system manage the address space of processes?
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
};
</pid>
因此,进程地址空间实际上就是结构体mm_struct所描述的虚拟空间,每个进程都有自己的虚拟地址空间。每个进程的虚拟地址如下图所示。
在Linux中,采用分页存储的方式对内存进行管理。既然我们平时所看到的地址不是实际的物理地址,那就需要操作系统将虚拟地址映射为物理地址。操作系统是借助页表来实现虚拟地址和物理地址的映射的,页表的本质也是一个数据结构,最主要的两项就是进程的虚拟地址和实际物理地址的映射关系。
6.2 写时拷贝
在我们的代码中,当fork创建子进程时,会将父进程的mm_struct也拷贝给子进程,一开始,内存中只有一份g_val,当子进程修改g_val时,由于父子进程的数据是各自私有的,进程之间的执行应该具有独立性,因此子进程修改g_val不应该影响到父进程。此时就会发生 写时拷贝,即子进程在内存中开辟一块新的空间,将修改后的值填入该空间,并且修改子进程页表中虚拟地址映射的实际物理地址。
结果,我们看到了存储在同一虚拟地址中的值不同的情况。
[En]
As a result, we see a scenario in which the values stored in the same virtual address are different.
6.3 为什么要有进程地址空间?
这是因为引入了进程地址空间后,可以保证每个进程所用的空间独立而连续3。一个进程的越界操作并不会影响另一个进程,这样就实现了内存的保护。同时,每个进程地址空间是远大于实际内存空间的,这样也可以通过虚拟的方式实现内存的扩充。当一个进程退出后,我们只需要清除掉该进程的mm_struct和页表就可,有利于内存的分配回收。
Original: https://www.cnblogs.com/Grong/p/15516013.html
Author: 乌有先生ii
Title: Linux系统编程之进程概念
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/524395/
转载文章受原作者版权保护。转载请注明原作者出处!