文章目录
  1. 进程创建
    1. 进程的地址空间
    2. fork与写时复制
    3. vfork
    4. 创建线程
    5. 小结
  2. 进程加载
  3. 进程切换
  4. 进程等待与退出

进程创建

对于unix类系统,创建进程采用fork()与exec()两个系统调用来完成;

  • fork()将一个进程复制为两个进程;被复制的进程称为父进程,复制得到的进程称为子进程;复制的信息包括父进程的管理结构,线性地址空间与除eax(eax置0)外所有寄存器的值;
  • exec()用新程序来重写当前进程;

Q:在linux系统中,如果任何一个创建的进程都必须(在用户态)通过复制其他进程得到,那么第一个进程是怎么来的呢?

A:第一个进程由操作系统的设计者写好,这个进程就是进程0。

进程的地址空间

在介绍fork创建进程之前,先说明Linux是怎么表示一个进程的——task_struct结构体,在Linux2.2.0的源码中,task_struct的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
struct task_struct {
/* these are hardcoded - don't touch */
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
unsigned long flags; /* per process flags, defined below */
int sigpending;
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
struct exec_domain *exec_domain;
long need_resched;
/* various fields */
long counter;
long priority;
cycles_t avg_slice;
/* SMP and runqueue state */
int has_cpu;
int processor;
int last_processor;
int lock_depth; /* Lock depth. We can context switch in and out of holding a syscall kernel lock... */
struct task_struct *next_task, *prev_task;
struct task_struct *next_run, *prev_run;
/* task state */
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned long personality;
int dumpable:1;
int did_exec:1;
pid_t pid;
pid_t pgrp;
pid_t tty_old_pgrp;
pid_t session;
/* boolean value for session group leader */
int leader;
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->p_pptr->pid)
*/
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
/* PID hash table linkage. */
struct task_struct *pidhash_next;
struct task_struct **pidhash_pprev;
/* Pointer to task[] array linkage. */
struct task_struct **tarray_ptr;
struct wait_queue *wait_chldexit; /* for wait4() */
struct semaphore *vfork_sem; /* for vfork() */
unsigned long policy, rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
struct tms times;
unsigned long start_time;
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
unsigned long swap_address;
unsigned long swap_cnt; /* number of pages to swap on next pass */
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups;
gid_t groups[NGROUPS];
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
struct user_struct *user;
/* limits */
struct rlimit rlim[RLIM_NLIMITS];
unsigned short used_math;
char comm[16];
/* file system info */
int link_count;
struct tty_struct *tty; /* NULL if no tty */
/* ipc stuff */
struct sem_undo *semundo;
struct sem_queue *semsleeping;
/* tss for this task */
struct thread_struct tss;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* memory management info */
struct mm_struct *mm;
/* signal handlers */
spinlock_t sigmask_lock; /* Protects signal and blocked */
struct signal_struct *sig;
sigset_t signal, blocked;
struct signal_queue *sigqueue, **sigqueue_tail;
unsigned long sas_ss_sp;
size_t sas_ss_size;
};

其中,结构体mm_struct用来描述一个进程的虚拟地址空间。mm_struct的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct vm_area_struct *mmap_avl; /* tree of VMAs */
struct vm_area_struct *mmap_cache; /* last find_vma result */
pgd_t * pgd;
atomic_t count;
int map_count; /* number of VMAs */
struct semaphore mmap_sem;
unsigned long context;
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 rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
/*
* This is an architecture-specific pointer: the portable
* part of Linux does not know about any segments.
*/
void * segments;
};

其中vm_area_struct用来描述一个虚拟内存区域VMA,一个进程的地址空间由多个虚拟内存区域组成,各虚拟内存区域由链表组织。操作系统将每个内存区域作为一个单独的内存对象管理,每个内存区域都有一致的属性(因此进程的代码段、数据段、bss段都分别用一个vm_area_struct描述)。vm_area_struct的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;

pgprot_t vm_page_prot;
unsigned short vm_flags;

/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;

/* For areas with inode, the list inode->i_mmap, for shm areas,
* the list of attaches, otherwise unused.
*/
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;

struct vm_operations_struct * vm_ops;
unsigned long vm_offset;
struct file * vm_file;
unsigned long vm_pte; /* shared mem */
};

最终,进程管理其进程空间的实现如下图所示。

 

fork与写时复制

传统的fork直接将父进程所有资源复制给子进程,很多时候,操作系统创建一个进程是为了加载一个新的可执行文件,此时复制父进程资源是没有必要的。于是之后的fork采用写时复制技术:创建的子进程以只读方式共享父进程的地址空间(实现方式为仅复制父进程的页表而不复制对应物理内存区域中的内容),如果父/子进程需要向地址空间写入数据,操作系统再去复制一份地址空间的内容供父/子进程修改。这时,如果fork之后立即执行了exec,则无需复制父进程的地址空间的内容。此外,之后的fork还明确子进程先执行。

vfork

vfork创建的子进程相当于父进程的线程,它没有复制父进程的页表,直接共用父进程的地址空间,即子进程的task_struct直接复制父进程的task_struct的mm成员,注意到mm是一个mm_struct结构体指针,因此vfork是让子进程共用父进程的地址空间。此外,vfork明确子进程先执行。使用vfork创建子进程后,父进程一直处于阻塞状态,直到子进程退出或执行完exec。

创建线程

通过创建与父进程共享某些资源(具体见下表)的子进程,来创建线程。

 

小结

总结一下创建进程的完整过程;

  • 分配进程控制块数据结构;
  • 创建进程的内核堆栈;
  • 设置进程的地址空间(如果是创建线程则共享父进程的地址空间);
  • 修改子进程的状态为不可中断等待状态(还未进行进程加载);

进程加载

将刚刚分配子进程的地址空间重写,其中包括代码段、数据段、堆与栈等等;使用exec调用来实现;

进程切换

暂停当前运行进程(当前运行进程从运行态变为其他状态),调度另一个进程(也有可能还是刚刚运行的进程)从就绪状态变成运行状态。

进程切换前,保存被暂停进程的上下文(寄存器、CPU状态);进程切换后,恢复被调度进程的上下文(寄存器、CPU状态)。

如下是一个进程切换的例子:

 

为了对进程进行切换,操作系统将相同状态的PCB放置在同一队列。如下图所示:有就绪(进程)队列、等待(进程)队列以及僵尸队列(要退出的进程)。

 

当并发的进程数目太多时,使用双向链表来组织PCB使得查找的时间过长,那么这时采用Hash表,Hash值相同PCB组成双向链表,再通过数组将这些双向链表组织起来。

 

实现进程调度的函数为schedule。

进程等待与退出

wait系统调用用于父进程等待子进程的结束;子进程结束时通过exit()向父进程返回一个值,父进程通过wait()接受并处理返回值。

  • 当子进程存活时,如果父进程调用了wait,则父进程进入等待状态,等待子进程的返回结果。当子进程结束调用了exit()后,父进程被唤醒,将子进程exit()的返回值作为父进程wait()的返回值;
  • 如果父进程调用wait时已有结束的子进程,wait()立即返回这些已经结束的子进程调用exit()返回值中的一个;
  • 若无子进程存活,wait()立刻返回;

exit被称为有序终止;

  • exit()返回的结果作为父进程下一步处理的参数;
  • 进程各种资源的回收(打开的文件,分配的内存空间以及大部分与进程相关的数据结构);
  • 清理所有处于僵尸状态的子进程;
  • 检查父进程是否存活,若父进程存活,自身进入僵尸状态,等待父进程处理;否则,释放所有的数据结构,进程结束;