第二章 进程
本章主要是进程的介绍,关于进程的调度算法在后续专门的章节介绍
1.进程概念和分类
进程是程序执行时的一个实例,可以把它看作充分描述程序已经执行到何种程度的数据结构的汇集,多个进程可能共用一个可执行代码,从内核观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体。
线程是多线程应用程序中的概念,即一个进程由几个线程组成,每个线程代表进程的一个执行流,每个线程共享该进程的大部分数据结构,从内核角度看,多线程应用程序仅仅是一个普通进程,因为其多个执行流的创建、处理、调整都是在用户态进行的,即它们彼此之间的关系是用户态规定的,内核不可见。
为了更好地实现多线程应用程序,linux提供了轻量级进程,其特点是可以共享一些资源,且只要其中一个修改共享资源,另一个就立即查看这种修改,同时每个轻量级进程都可以由内核独立调度,以便一个睡眠的同时另一个仍然是可运行的,实现多线程应用程序一个简单方式就是把轻量级进程与每个线程关联起来。最后,linux中一个线程组基本上就实现了多线程应用的一组轻量级进程。待补充:找一个多线程应用程序的创建实例代码。
2.进程描述符
对每个进程所做的事情进行清除地描述的数据结构task_struct,结构如下:
structtask_struct {
volatilelong state; /* -1 unrunnable, 0 runnable, >0 stopped */
void*stack;
atomic_tusage;
unsignedint flags; /* per process flags, defined below */
unsignedint ptrace;
#ifdefCONFIG_SMP
structllist_node wake_entry;
inton_cpu;
structtask_struct *last_wakee;
unsignedlong wakee_flips;
unsignedlong wakee_flip_decay_ts;
intwake_cpu;
#endif
inton_rq;
intprio, static_prio, normal_prio;
unsignedint rt_priority;
conststruct sched_class *sched_class;
structsched_entity se;
structsched_rt_entity rt;
#ifdefCONFIG_CGROUP_SCHED
structtask_group *sched_task_group;
#endif
#ifdefCONFIG_PREEMPT_NOTIFIERS
/*list of struct preempt_notifier: */
structhlist_head preempt_notifiers;
#endif
#ifdefCONFIG_BLK_DEV_IO_TRACE
unsignedint btrace_seq;
#endif
unsignedint policy;
intnr_cpus_allowed;
cpumask_tcpus_allowed;
#ifdefCONFIG_PREEMPT_RCU
intrcu_read_lock_nesting;
charrcu_read_unlock_special;
structlist_head rcu_node_entry;
#endif/* #ifdef CONFIG_PREEMPT_RCU */
#ifdefCONFIG_TREE_PREEMPT_RCU
structrcu_node *rcu_blocked_node;
#endif/* #ifdef CONFIG_TREE_PREEMPT_RCU */
#ifdefCONFIG_RCU_BOOST
structrt_mutex *rcu_boost_mutex;
#endif/* #ifdef CONFIG_RCU_BOOST */
#ifdefined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
structsched_info sched_info;
#endif
structlist_head tasks;
#ifdefCONFIG_SMP
structplist_node pushable_tasks;
#endif
structmm_struct *mm, *active_mm;
#ifdefCONFIG_COMPAT_BRK
unsignedbrk_randomized:1;
#endif
#ifdefined(SPLIT_RSS_COUNTING)
structtask_rss_stat rss_stat;
#endif
/*task state */
intexit_state;
intexit_code, exit_signal;
intpdeath_signal; /* The signal sent when the parent dies */
unsignedint jobctl; /* JOBCTL_*, siglock protected */
/*Used for emulating ABI behavior of previous Linux versions */
unsignedint personality;
unsigneddid_exec:1;
unsignedin_execve:1; /* Tell the LSMs that the process is doing an
* execve */
unsignedin_iowait:1;
/*task may not gain privileges */
unsignedno_new_privs:1;
/*Revert to default priority/policy when forking */
unsignedsched_reset_on_fork:1;
unsignedsched_contributes_to_load:1;
pid_tpid;
pid_ttgid;
#ifdefCONFIG_CC_STACKPROTECTOR
/*Canary value for the -fstack-protector gcc feature */
unsignedlong stack_canary;
#endif
/*
* pointers to (original) parent process, youngest child, youngersibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
structtask_struct __rcu *real_parent; /* real parent process */
structtask_struct __rcu *parent; /* recipient of SIGCHLD, wait4()reports */
/*
* children/sibling forms the list of my natural children
*/
structlist_head children; /* list of my children */
structlist_head sibling; /* linkage in my parent's children list */
structtask_struct *group_leader; /* threadgroup leader */
/*
* ptraced is the list of tasks this task is using ptrace on.
* This includes both natural children and PTRACE_ATTACH targets.
* p->ptrace_entry is p's link on the p->parent->ptracedlist.
*/
structlist_head ptraced;
structlist_head ptrace_entry;
/*PID/PID hash table linkage. */
structpid_link pids[PIDTYPE_MAX];
structlist_head thread_group;
structcompletion *vfork_done; /* for vfork() */
int__user *set_child_tid; /* CLONE_CHILD_SETTID */
int__user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
cputime_tutime, stime, utimescaled, stimescaled;
cputime_tgtime;
#ifndefCONFIG_VIRT_CPU_ACCOUNTING_NATIVE
structcputime prev_cputime;
#endif
#ifdefCONFIG_VIRT_CPU_ACCOUNTING_GEN
seqlock_tvtime_seqlock;
unsignedlong long vtime_snap;
enum{
VTIME_SLEEPING= 0,
VTIME_USER,
VTIME_SYS,
}vtime_snap_whence;
#endif
unsignedlong nvcsw, nivcsw; /* context switch counts */
structtimespec start_time; /* monotonic time */
structtimespec real_start_time; /* boot based time */
/*mm fault and swap info: this can arguably be seen as eithermm-specific or thread-specific */
unsignedlong min_flt, maj_flt;
structtask_cputime cputime_expires;
structlist_head cpu_timers[3];
/*process credentials */
conststruct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
conststruct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
charcomm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
/*file system info */
intlink_count, total_link_count;
#ifdefCONFIG_SYSVIPC
/*ipc stuff */
structsysv_sem sysvsem;
#endif
#ifdefCONFIG_DETECT_HUNG_TASK
/*hung task detection */
unsignedlong last_switch_count;
#endif
/*CPU-specific state of this task */
structthread_struct thread;
/*filesystem information */
structfs_struct *fs;
/*open file information */
structfiles_struct *files;
/*namespaces */
structnsproxy *nsproxy;
/*signal handlers */
structsignal_struct *signal;
structsighand_struct *sighand;
sigset_tblocked, real_blocked;
sigset_tsaved_sigmask; /* restored if set_restore_sigmask() was used */
structsigpending pending;
unsignedlong sas_ss_sp;
size_tsas_ss_size;
int(*notifier)(void *priv);
void*notifier_data;
sigset_t*notifier_mask;
structcallback_head *task_works;
structaudit_context *audit_context;
#ifdefCONFIG_AUDITSYSCALL
kuid_tloginuid;
unsignedint sessionid;
#endif
structseccomp seccomp;
/*Thread group tracking */
u32parent_exec_id;
u32self_exec_id;
/*Protection of (de-)allocation: mm, files, fs, tty, keyrings,mems_allowed,
*mempolicy */
spinlock_talloc_lock;
/*Protection of the PI data structures: */
raw_spinlock_tpi_lock;
#ifdefCONFIG_RT_MUTEXES
/*PI waiters blocked on a rt_mutex held by this task */
structplist_head pi_waiters;
/*Deadlock detection and priority inheritance handling */
structrt_mutex_waiter *pi_blocked_on;
#endif
#ifdefCONFIG_DEBUG_MUTEXES
/*mutex deadlock detection */
structmutex_waiter *blocked_on;
#endif
#ifdefCONFIG_TRACE_IRQFLAGS
unsignedint irq_events;
unsignedlong hardirq_enable_ip;
unsignedlong hardirq_disable_ip;
unsignedint hardirq_enable_event;
unsignedint hardirq_disable_event;
inthardirqs_enabled;
inthardirq_context;
unsignedlong softirq_disable_ip;
unsignedlong softirq_enable_ip;
unsignedint softirq_disable_event;
unsignedint softirq_enable_event;
intsoftirqs_enabled;
intsoftirq_context;
#endif
#ifdefCONFIG_LOCKDEP
#define MAX_LOCK_DEPTH 48UL
u64curr_chain_key;
intlockdep_depth;
unsignedint lockdep_recursion;
structheld_lock held_locks[MAX_LOCK_DEPTH];
gfp_tlockdep_reclaim_gfp;
#endif
/*journalling filesystem info */
void*journal_info;
/*stacked block device info */
structbio_list *bio_list;
#ifdefCONFIG_BLOCK
/*stack plugging */
structblk_plug *plug;
#endif
/*VM state */
structreclaim_state *reclaim_state;
structbacking_dev_info *backing_dev_info;
structio_context *io_context;
unsignedlong ptrace_message;
siginfo_t*last_siginfo; /* For ptrace use. */
structtask_io_accounting ioac;
#ifdefined(CONFIG_TASK_XACCT)
u64acct_rss_mem1; /* accumulated rss usage */
u64acct_vm_mem1; /* accumulated virtual memory usage */
cputime_tacct_timexpd; /* stime + utime since last update */
#endif
#ifdefCONFIG_CPUSETS
nodemask_tmems_allowed; /* Protected by alloc_lock */
seqcount_tmems_allowed_seq; /* Seqence no to catch updates */
intcpuset_mem_spread_rotor;
intcpuset_slab_spread_rotor;
#endif
#ifdefCONFIG_CGROUPS
/*Control Group info protected by css_set_lock */
structcss_set __rcu *cgroups;
/*cg_list protected by css_set_lock and tsk->alloc_lock */
structlist_head cg_list;
#endif
#ifdefCONFIG_FUTEX
structrobust_list_head __user *robust_list;
#ifdefCONFIG_COMPAT
structcompat_robust_list_head __user *compat_robust_list;
#endif
structlist_head pi_state_list;
structfutex_pi_state *pi_state_cache;
#endif
#ifdefCONFIG_PERF_EVENTS
structperf_event_context *perf_event_ctxp[perf_nr_task_contexts];
structmutex perf_event_mutex;
structlist_head perf_event_list;
#endif
#ifdefCONFIG_NUMA
structmempolicy *mempolicy; /* Protected by alloc_lock */
shortil_next;
shortpref_node_fork;
#endif
#ifdefCONFIG_NUMA_BALANCING
intnuma_scan_seq;
unsignedint numa_scan_period;
unsignedint numa_scan_period_max;
intnuma_preferred_nid;
intnuma_migrate_deferred;
unsignedlong numa_migrate_retry;
u64node_stamp; /* migration stamp */
structcallback_head numa_work;
structlist_head numa_entry;
structnuma_group *numa_group;
/*
* Exponential decaying average of faults on a per-node basis.
* Scheduling placement decisions are made based on the these counts.
* The values remain static for the duration of a PTE scan
*/
unsignedlong *numa_faults;
unsignedlong total_numa_faults;
/*
* numa_faults_buffer records faults per node during the current
* scan window. When the scan completes, the counts in numa_faults
* decay and these values are copied.
*/
unsignedlong *numa_faults_buffer;
/*
* numa_faults_locality tracks if faults recorded during the last
* scan window were remote/local. The task scan period is adapted
* based on the locality of the faults with different weights
* depending on whether they were shared or private faults
*/
unsignedlong numa_faults_locality[2];
unsignedlong numa_pages_migrated;
#endif/* CONFIG_NUMA_BALANCING */
structrcu_head rcu;
/*
* cache last used pipe for splice
*/
structpipe_inode_info *splice_pipe;
structpage_frag task_frag;
#ifdef CONFIG_TASK_DELAY_ACCT
structtask_delay_info *delays;
#endif
#ifdefCONFIG_FAULT_INJECTION
intmake_it_fail;
#endif
/*
* when (nr_dirtied >= nr_dirtied_pause), it's time to call
* balance_dirty_pages() for some dirty throttling pause
*/
intnr_dirtied;
intnr_dirtied_pause;
unsignedlong dirty_paused_when; /* start of a write-and-pause period */
#ifdefCONFIG_LATENCYTOP
intlatency_record_count;
structlatency_record latency_record[LT_SAVECOUNT];
#endif
/*
* time slack values; these are used to round up poll() and
* select() etc timeout values. These are in nanoseconds.
*/
unsignedlong timer_slack_ns;
unsignedlong default_timer_slack_ns;
#ifdefCONFIG_FUNCTION_GRAPH_TRACER
/*Index of current stored address in ret_stack */
intcurr_ret_stack;
/*Stack of return addresses for return function tracing */
structftrace_ret_stack *ret_stack;
/*time stamp for last schedule */
unsignedlong long ftrace_timestamp;
/*
* Number of functions that haven't been traced
* because of depth overrun.
*/
atomic_ttrace_overrun;
/*Pause for the tracing */
atomic_ttracing_graph_pause;
#endif
#ifdefCONFIG_TRACING
/*state flags for use by tracers */
unsignedlong trace;
/*bitmask and counter of trace recursion */
unsignedlong trace_recursion;
#endif/* CONFIG_TRACING */
#ifdefCONFIG_MEMCG /* memcg uses this to do batch job */
structmemcg_batch_info {
intdo_batch; /* incremented when batch uncharge started */
structmem_cgroup *memcg; /* target memcg of uncharge */
unsignedlong nr_pages; /* uncharged usage */
unsignedlong memsw_nr_pages; /* uncharged mem+swap usage */
}memcg_batch;
unsignedint memcg_kmem_skip_account;
structmemcg_oom_info {
structmem_cgroup *memcg;
gfp_tgfp_mask;
intorder;
unsignedint may_oom:1;
}memcg_oom;
#endif
#ifdefCONFIG_UPROBES
structuprobe_task *utask;
#endif
#ifdefined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)
unsignedint sequential_io;
unsignedint sequential_io_avg;
#endif
};
进程描述符中存放了很多信息,是相当复杂的,它不仅包含了很多进程属性的字段,而且一些字段还包含了指向其他数据结构的指针,其中几个数据结构涉及进程所拥有的特殊资源,如第二个stack也叫thread_info表示进程的基本信息,mm_struct指向内存区描述符的指针,fs_struct表示当前目录,files_struct指向文件描述符的指针,signal_struct表示所接收到的信号,这些资源在后续章节涉及到会详细描述。这里讨论2种字段进程的状态和进程的父/子间关系。
进程描述符中第一个字段state即表示进程的状态,可分为:可运行状态(TASK_RUNNING),可中断的等待状态,不可中断的等待状态,暂停状态,跟踪状态,僵死状态僵死撤销状态。其中等待状态即被挂起(睡眠),可能在等待某个资源,一旦资源获取到了就会被唤醒放回到可运行状态,或者收到一个信号也可以唤醒。其中不可中断的区别在于收到信号不能被唤醒,必须等到一个不能被中断的事件发生才能被唤醒,这里不能中断的是等待状态,如驱动程序(进程)要先探测硬件设备(等待状态),获取到硬件状态结果后才能执行驱动程序,如下图中也被称作浅度睡眠和深度睡眠,浅度睡眠的进程被CFS调度选中唤醒,深度睡眠进程由于信号量,锁等的释放而被唤醒,或中断、异常唤醒。暂停和跟踪状态可以理解为*暂停,而前面的等待状态则为主动暂停。僵死状态是被终止的进程但还没有被内核删除数据的状态,因为此时还没有上报给父进程相关的信息,僵死撤销状态是在父进程获取死亡进程消息时防止其他进程也来获取信息的一种状态,总之只要有状态,说明该进程并没有真正消失。
进程间的关系,通过给定进程P的进程描述符中表示进程关系的字段,real_parent指向创建了P的进程的描述符,parent指向P的当前父进程,和real_parent的区别在于另一个进程发出监控P的ptrace系统调用请求时,children是链表的头部,该链表中所有元素都是P创建的子进程,sibling指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程都是P,这个变量和linux2.6.22有区别待理解。还有其他表示非亲属关系的字段,如所在进程组领头进程,登录绘画领头进程等。
标识一个进程,内核对进程的大部分引用是通过进程描述符指针进行的,另一种方法是通过PID号将每个进程或轻量级进程区分开,可以被唯一识别,进程描述符中tgid字段表示其所在线程组共用的PID也是领头线程的PID,而pid字段表示其特有的PID号,所以领头线程的tgid值和pid值相同。
如何快速地获取当前在CPU上正在运行进程的进程描述符指针,首先进程是动态实体有生命周期,进程描述符需要存放在动态内存中,为了实现快速找到进程描述符指针,linux采用将进程描述符中小数据结构thread_info线程描述符和内核态的进程堆栈紧凑地存放在一个单独为进程分配的存储区域,大小为2个连续的页框(8192个字节),也就是说整个进程描述符的空间动态分配后,再将其第一个成员变量赋值为另一个动态分配的内存地址,再说这个紧凑地2个页框,存放方式是线程描述符驻留于这个8K内存区的开始,而栈从内存区的末端向下增长,而esp寄存器是CPU栈指针,用来存放栈顶单元的地址,一旦数据写入堆栈,esp寄存器值就递减,通过使用联合体表示一个线程描述符和内核栈,这样想要获取线程描述符的地址只需要将esp寄存器中的低13位设为0(8K=2^13字节)即可,最终是要获取进程描述符指针,而线程描述符第一个成员task指向进程描述符,由于偏移量为0,所以前面获取的线程描述符基地址即为进程描述符的基地址。
如何从进程的PID号导出对应的进程描述符指针需要考虑,如在kill系统调用时就会用到,顺序扫描进程双向循环链表并检查进程描述符的pid字段是可行但相当低效,为了加速查找,引入了4个散列表,分别对应不同类型的PID,这部分待理解。
如何组织进程,这里引入了几个链表,包括进程链表,可运行进程的链表即运行队列,(这里采用将不同进程优先级k(0~139)分别对应不同的运行队列,这样调度程序可以在固定时间内选出最佳可运行进程,与可运行进程数无关,这是通过使数据结构更复杂来改善性能),处在等待状态的进程链表即等待队列,根据等待某个不同的事件将等待队列分成许多细类,这样满足快速检索的需要,等待队列的操作:
数据结构的定义和初始化,等待队列由双向链表实现,每个等待队列由一个等待队列头代表,只要通过它就可以找到对应的等待队列,等待队列由等待队列元素组成,下面分别是2种数据结构的组成:等待队列头第一个成员是自旋锁,因为等待队列是由中断处理程序和主要内核函数修改的,必须对齐进行同步保护,第二个成员是等待队列链表的头,可以理解为起到链接到等待队列中第一个元素的作用;
struct__wait_queue_head {
spinlock_t lock;
structlist_head task_list;
};
等待队列元素成员flags表示该进程是否等待互斥访问某一要释放的资源1是0否,如果是互斥的则在系统资源释放时只能释放其中一个进程,非互斥则可唤醒等待队列中所有进程,private即为进程描述符指针task_struct指向睡眠的进程,func则代表不同的唤醒策略函数,task_list把该队列元素链接到相同队列其他元素。
struct__wait_queue {
unsignedint flags;
#defineWQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
structlist_head task_list;
};
下面是初始化等待队列元素的函数
staticinline void init_waitqueue_entry(wait_queue_t *q, struct task_struct*p)
{
q->flags =0;
q->private =p;
q->func =default_wake_function;
}
还有操作等待队列的相关函数
staticinline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t*new)
{
list_add(&new->task_list,&head->task_list);
}把一个非互斥进程插入等待队列链表的第一个位置
staticinline void
__add_wait_queue_tail_exclusive(wait_queue_head_t*q, wait_queue_t *wait)
{
wait->flags|= WQ_FLAG_EXCLUSIVE;
__add_wait_queue_tail(q,wait);
}将一个互斥进程插入等待队列的最后一个位置,这里主要利用链表的一些原语操作,具体可以查看list.h
上面是关于等待队列的一些操作,那么进程的睡眠和唤醒过程如何操作这些等待队列则需要另外一些函数,诸如:睡眠函数sleep_on,interruptible_sleep_on,sleep_on_timeout唤醒函数wake_up,wake_up_nr,wake_up_all,wake_up_locked,wake_up_all_locked,wake_up_interruptible,wake_up_interruptible_nr,wake_up_interruptible_all,wake_up_interruptible_sync,其中wake_up通用处理如下
staticvoid __wake_up_common(wait_queue_head_t *q, unsigned int mode,
intnr_exclusive, int sync, void *key)
{
structlist_head *tmp, *next;
list_for_each_safe(tmp,next, &q->task_list) {
wait_queue_t*curr = list_entry(tmp, wait_queue_t, task_list);
unsignedflags = curr->flags;
if(curr->func(curr, mode, sync, key) &&
(flags& WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}扫描等待队列双向链表q->task_list中的所有项,这里的每一项tmp都是list_head类型的指针,这些list_head指针都是每个wait_queue_t元素插入等待队列的,所以通过list_entry函数将特定list_head指针(tmp)、拥有该指针的结构体类型(wait_queue_t)和该指针在结构体中的位置(task_list)作为入参就可以计算出等待队列中某一个节点对应的wait_queue_t变量地址,而每个wait_queue_t变量里包含了一个等待的进程,找到这个变量调用其func字段存放的唤醒函数试图唤醒wait_queue_t变量中task字段对应的进程,如果一个进程被有效地唤醒并且进程是互斥的且剩下互斥进程的个数为0,循环结束,因为所有的非互斥进程总是在双向链表的开始位置,而所有的互斥进程在链表的尾部,所以函数总是唤醒非互斥进程然后再唤醒互斥进程。
3.进程切换
进程切换:挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。而进程恢复执行前必须装入寄存器的一组数据称为硬件上下文,在进程切换时的一个主要动作就是硬件上下文的切换。早期的linux版本利用80x86体系结构所提供的硬件指令进行硬件上下文切换,硬件上下文的一部分存放在TSS(任务状态段),剩余部分存放在内核堆栈中,硬件指令farjmp直接跳到被执行进程的TSS描述符来执行进程切换,而linux2.6使用软件执行进程的切换,通过mov指令逐步执行切换,这样能较好地控制所装入数据的合法性,即可对每一步获取和赋值的寄存器值进行检查。由于采用软件执行硬件上下文的切换,所以linux为系统中每个不同的cpu创建一个TSS而不是每个进程,这个TSS被用作其他用途,而每次进程切换时,被替换的进程的硬件上下文的一部分被保存在进程描述符成员中的thread_struct结构中,剩余部分被保留在内核堆栈中。
进程切换发生在shcedule()函数里,由2步组成:
1)切换页全局目录以安装一个新的地址空间,在第9章详细描述
2)切换内核态堆栈和硬件上下文
这里进程切换的第2步由switch_to宏执行,分析其实现的策略,该宏有3个参数,prev,next和last,prev和next分别表示被替换进程和新进程描述符的地址在内存中的位置这些变量是局部变量,在每个进程的内核堆栈中分配的,假设此时的调度场景是暂停进程A而**进程B,那么在A进程内核堆栈中分配的prev指向A的描述符而next指向B的描述符,那么last指向谁呢,再假设后续的调度场景,当内核想再次**A而暂停另一个进程C(这通常不同于B),而这时C的内核堆栈中的prev指向C而next指向A,来执行另一个switch_to宏,等到A恢复它的执行流时,就会找到它原来的内核堆栈中存放的prev和next局部变量,而这2个变量存的地址分别是A和B的,这样A没有将C进程的描述符记录下来,
就失去了对C的任何引用,其实last就是A的局部变量用来保存C进程(即因被**进程而挂起的那个进程)描述符的地址。也就是说switch_to宏整个执行是由C进程开始调用,后面又切换到A进程执行,但是此时switch_to的执行还没结束且需要C进程的信息,由于内核栈处在A进程,所以通过prev访问不到C进程的描述符,这时在切换到A进程之后将C进程的描述符地址存放在last局部变量里以供后续完成进程切换使用(这几个局部变量在内核栈中的分布和如何被调用需要详细的内存分布图待理解补充)。
具体汇编代码实现的步骤如下:
1)保存prev和next的值到eax和edx寄存器
2)保存硬件上下文eflags和ebp寄存器内容到prev内核栈
3)新旧栈切换,即保存硬件上下文esp寄存器的内容(指向prev内核栈的栈顶)到prev->thread.esp中,恢复next的内核栈栈顶地址到esp寄存器
4)新旧指令地址切换,保存标记为1的指令地址到prev->thread.eip,并将next->thread.eip的值装入内核栈,切换到新进程的执行流
5)跳到__switch_toC函数执行
6)拷贝eax寄存器的内容到last标识的内存区域中
__switch_toC函数具体实现一些保存和恢复操作,不再详述。
4.创建进程
创建进程由clone函数实现,该函数是C语言库中的封装函数,它会调用clone系统调用,fork和vfork系统调用也通过clone系统调用实现,差别是传入的flags不同。
clone系统调用会调用do_fork处理,do_fork的核心函数是copy_process主要是创建子进程的数据结构并复制父进程的数据内容到对应的子进程的数据结构,其中包括主要的进程描述符task_struct结构,do_fork的其他处理则是在copy_process子进程资源分配完成之后将其运行起来。do_fork的大致执行步骤如下:
1)通过查找pidmap_array位图,位子进程分配新的PID
2)检查父进程的ptrace字段,即是否有一个进程在跟踪父进程,决定是否设置CLONE_PTRACE标志
3)调用copy_process()分配复制进程描述符即其他数据结构
4)如果CLONE_STOPPED标志被设置或子进程被跟踪,则子进程的状态设成TASK_STOPPED,即挂起子进程并增加挂起的SIGSTOP信号,直到另一个进程把子进程状态恢复为TASK_RUNNING状态(通常是通过发送SIGCONT信号)才可以恢复执行。
5)如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task()执行子进程
6)决定是否设置TASK_STOPPED状态
7)如果父进程被跟踪,则调用ptrace_notify()通知debugger进程
8)如果设置了CLONE_VFORK标志,则把父进程插入等待队列并挂起父进程直到子进程执行结束,因为vfork系统调用父子进程共享内存地址空间防止父进程重写子进程需要的数据才这样做
9)结束并返回子进程的PID。
longdo_fork(unsigned long clone_flags,
unsignedlong stack_start,
structpt_regs *regs,
unsignedlong stack_size,
int__user *parent_tidptr,
int__user *child_tidptr)
{
structtask_struct *p;
inttrace = 0;
structpid *pid = alloc_pid();
longnr;
if(!pid)
return-EAGAIN;
nr= pid->nr;
if(unlikely(current->ptrace)) {
trace= fork_traceflag (clone_flags);
if(trace)
clone_flags|= CLONE_PTRACE;
}
p= copy_process(clone_flags, stack_start, regs, stack_size,parent_tidptr, child_tidptr, pid);
/*
*Do this prior waking up the new thread - the thread pointer
*might get invalid after that point, if the thread exits quickly.
*/
if(!IS_ERR(p)) {
structcompletion vfork;
if(clone_flags & CLONE_VFORK) {
p->vfork_done= &vfork;
init_completion(&vfork);
}
if((p->ptrace & PT_PTRACED) || (clone_flags &CLONE_STOPPED)) {
/*
*We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal,SIGSTOP);
set_tsk_thread_flag(p,TIF_SIGPENDING);
}
if(!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p,clone_flags);
else
p->state= TASK_STOPPED;
if(unlikely (trace)) {
current->ptrace_message= nr;
ptrace_notify((trace << 8) | SIGTRAP);
}
if(clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
if(unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
current->ptrace_message= nr;
ptrace_notify((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
}else {
free_pid(pid);
nr= PTR_ERR(p);
}
returnnr;
}
copy_process()函数主要执行包括安全检查security_task_create(),获取进程描述符dup_task_struct(),初始化list_head和自旋锁,调用一系列拷贝函数创建新的数据结构并复制父进程的内容,如copy_files,copy_mm,其他处理细节可以查看代码。
遗留问题:
为什么子进程要拷贝父进程的资源,它们执行不同的任务为什么会使用相同的信息?既然是拷贝,所以子进程还是要分配自己的数据结构,写时复制具体如何实现,是先访问一块公用内存,如果要写入,再将内存里的值保存到各自相应的数据结构中吗?轻量级父子进程共享数据结构如页表和打开文件表是怎么实现的,不需要再单独分配一个数据结构吗?vfork系统调用创建的子进程能共享内存地址空间和共享数据结构有什么区别,内存地址空间具体指的是哪一块空间?
5.撤销进程
进程的撤销分为进程终止和进程删除2步。
进程终止,主要是从内核数据结构中删除对终止进程的大部分引用并通知父进程。使用C库函数,exit()终止整个线程组,调用的系统调用exit_group,系统调用的内核函数为do_group_exit();pthread_exit()终止某一个线程,调用的系统调用exit(),系统调用调用的内核函数为do_exit()。do_group_exit()函数杀死属于current线程组的所有进程,主要任务是设置SIGNAL_GROUP_EXIT标志,调用do_exit()函数。do_exit()函数主要做下面任务:
1)分别调用exit_mm,exit_mm等等函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。
2)调用exit_notify()函数执行操作:跟新父子进程的亲属关系,检查exit_signal字段是否等于-1,并发送一个信号SIGNAL以通知父进程子进程死亡的消息,等于且没有被跟踪则设置exit_state为EXIT_DEAD,然后调用release_task()回收内存。不等于-1或进程正在被跟踪则设置exit_state为EXIT_ZOMBIE,在进程删除一步进一步处理。
3)调用schedule()函数选择一个新进程运行。
进程删除,针对僵死状态的进程做进一步处理,之所以引入僵死状态,是为了在子进程终止后保留进程描述符的数据,直到父进程发出wait()类系统调用知道了子进程是否终止。防止父进程在子进程结束之前结束导致这些子进程变成僵死的进程,所以必须强迫所有的孤儿进程成为init进程的子进程。
release_task()函数处理僵死进程进行进程描述符即内核态堆栈所占内存区域的释放,以及其他资源的释放。