Chinaunix首页 | 论坛 | 博客
  • 博客访问: 204989
  • 博文数量: 70
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 412
  • 用 户 组: 普通用户
  • 注册时间: 2013-08-30 11:07
文章分类

全部博文(70)

文章存档

2014年(68)

2013年(2)

我的朋友

分类: LINUX

2014-04-15 19:19:26

进程是所有操作系统的核心概念,同样在linux上也不例外。

主要内容:

  • 进程和线程
  • 进程的生命周期
  • 进程的创建
  • 进程的终止
一、进程和线程
1.定义:
进程:正在执行的程序代码的实时结果,包括可执行代码、资源(打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程、存放全局变量的数据段等)
线程:内核的调度对象,一种的特殊的进程,每个线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。

进程和线程是程序运行时状态,是动态变化的,进程和线程的管理操作(比如,创建,销毁等)都是有内核来实现的。

Linux中的进程于Windows相比是很轻量级的,而且不严格区分进程和线程,线程不过是一种特殊的进程。

所以下面只讨论进程,只有当线程与进程存在不一样的地方时才提一下线程。

2.进程提供2种虚拟机制:虚拟处理器和虚拟内存

每个进程有独立的虚拟处理器和虚拟内存,

每个线程有独立的虚拟处理器,同一个进程内的线程有可能会共享虚拟内存。

3.内核把进程的列表存放在任务队列(task list)的双向循环链表中。内核中的进程信息对应链表中的每一项粗放在类型为task_struct,称为进程描述符(process descriptor)结构。
进程标识PID(short int型:系统中允许同时存在的金成最大数最大为32768,/proc/sys/kernel/pid_max可修改)和线程标识TID对于同一个进程或线程来说都是相等的。POSIX标准中规定一个多线程应用程序中的所有线程都必须有相同的PID,在linux内核引入线程机制时,采用了线程组机制,同一线程组中的线程有相同的线程组号(Thread Group ID),tgid.线程组组号放在进程描述符的成员变量tgid中.

Linux中可以用ps命令查看所有进程的信息:

ps -eo pid,tid,ppid,comm



4.进程描述符及任务结构
task_struct:在32位机器上,大约有1.7KB。进程和线程都用其表示。linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色。

struct task_struct {

volatile long state; /*说明了该进程是否可以执行,还是可中断等信息*/
unsigned long flags; /*Flage 是进程号,在调用fork()时给出*/
int sigpending; /*进程上是否有待处理的信号*/
mm_segment_t addr_limit;
/**********************************************************/
/**进程地址空间,区分内核进程与普通进程在内存存放的位置不同*/
/****0-0xBFFFFFFF for user-thead    ***********************/
/****0-0xFFFFFFFF for kernel-thread ***********************/
/**********************************************************/
volatile long need_resched;
/**********************************************************/
/**********调度标志,表示该进程是否需要重新调度,************/
/**********若非0,则当从内核态返回到用户态,会发生调度*******/
/**********************************************************/

int lock_depth; /*********************锁深度***************/
long nice; /*************进程的基本时间片******************/
unsigned long policy;
/**********************************************************/
/*进程的调度策略,有三种************************************/
/*实时进程:SCHED_FIFO,SCHED_RR*****************************/
/*分时进程:SCHED_OTHER*************************************/
/**********************************************************/
/**********************************************************/
struct mm_struct *mm; //进程内存管理信息 
int processor;
/**********************************************************/
/*若进程不在任何CPU上运行,
/*cpus_runnable 的值是0,否则是1。
/*这个值在运行队列被锁时更新.*/
/**********************************************************/
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; /****指向运行队列的指针*********/
unsigned long sleep_time; /*****进程的睡眠时间*************/
struct task_struct *next_task, *prev_task;
/**********************************************************/
/*用于将系统中所有的进程连成一个双向循环链表*/
/*其根是init_task.*/ 
/**********************************************************/
struct mm_struct *active_mm; 
struct list_head local_pages;/**指向本地页面***************/
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt;/*进程所运行的可执行文件的格式*/ 
int exit_code, exit_signal; 
int pdeath_signal;/*父进程终止是向子进程发送的信号*********/ 
unsigned long personality;
/*Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序*/
int did_exec:1; 
/**********************************************************/
/*按POSIX要求设计的布尔量,区分进程正在执行从***************/
/*父进程中继承的代码,还是执行由execve装入的新程序代码******/ 
/**********************************************************/
pid_t pid;/**********进程标识符,用来代表一个进程***********/
pid_t pgrp;/********进程组标识,表示进程所属的进程组********/
pid_t tty_old_pgrp;/*******进程控制终端所在的组标识********/
pid_t session;/*************进程的会话标识*****************/
pid_t tgid;
int leader; /*************标志,表示进程是否为会话主管******/
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr; 
struct list_head thread_group; /****线程链表***************/
struct task_struct *pidhash_next;/*用于将进程链入HASH表pidhash 
struct task_struct **pidhash_pprev; 
wait_queue_head_t wait_chldexit; /*供wait4()使用***********/ 
struct completion *vfork_done; /* 供vfork() 使用***********/
unsigned long rt_priority;
/****实时优先级,用它计算实时进程调度时的weight值,/*******/
/*it_real_value,it_real_incr用于REAL定时器,单位为jiffies*/
系统根据it_real_value //设置定时器的第一个终止时间。
在定时器到期时,向进程发送SIGALRM信号,同时根据
it_real_incr重置终止时间,it_prof_value,it_prof_incr
用于Profile定时器,单位为jiffies。当进程运行时,
不管在何种状态下,每个tick都使it_prof_value值减一,
当减到0时,向进程发送信号SIGPROF,并根据it_prof_incr重置时间 
it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。
当进程运行时,不管在何种状态下,每个tick都使it_virt_value值减一
当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值。 
Real定时器根据系统时间实时更新,不管进程是否在运行 
Virtual定时器只在进程运行时,根据进程在用户态消耗的时间更新 
Profile定时器在进程运行时,根据进程消耗的时
(不管在用户态还是内核态)更新*****************************/ 
unsigned long it_real_value, it_prof_value, it_virt_value; 
unsigned long it_real_incr, it_prof_incr, it_virt_value; 
struct timer_list real_timer;//指向实时定时器的指针 
struct tms times; //记录进程消耗的时间, 
unsigned long start_time;//进程创建的时间 
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //记录进程在每个CPU上所消耗的用户态时间和核心态时间 
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ 
//内存缺页和交换信息: 
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换设备读入的页面数); 
//nswap记录进程累计换出的页面数,即写到交换设备上的页面数。 
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。在父进程 
//回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中 
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; 
int swappable:1; //表示进程的虚拟地址空间是否允许换出 
/* process credentials *////进程认证信息 
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid //euid,egid为有效uid,gid 
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们。 
//suid,sgid为备份uid,gid 
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;//进程的权能,分别是有效位集合,继承位集合,允许位集合 
int keep_capabilities:1; 
struct user_struct *user; 
/* limits */ 
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 
unsigned short used_math; //是否使用FPU 
char comm[16]; //进程正在运行的可执行文件名 
/* file system info *///文件系统信息 
int link_count, total_link_count; 
struct tty_struct *tty; /* NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空*/ 
unsigned int locks; /* How many file locks are being held */ 
/* ipc stuff *///进程间通信信息 
struct sem_undo *semundo; //进程在信号灯上的所有undo操作 
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作 
/* CPU-specific state of this task *///进程的CPU状态,切换时,要保存到停止进程的 
task_struct中 
struct thread_struct thread; 
/* filesystem information文件系统信息*/ 
struct fs_struct *fs; 
/* open file information *///打开文件信息 
struct files_struct *files; 
/* signal handlers *///信号处理函数 
spinlock_t sigmask_lock; /* Protects signal and blocked */ 
struct signal_struct *sig; //信号处理函数, 
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 
struct sigpending pending; //进程上是否有待处理的信号 
unsigned long sas_ss_sp; 
size_t sas_ss_size; 
int (*notifier)(void *priv); 
void *notifier_data; 
sigset_t *notifier_mask;/* Thread group tracking */ 
u32 parent_exec_id; 
u32 self_exec_id; 
/* Protection of (de-)allocation: mm, files, fs, tty */ 
spinlock_t alloc_lock; 
void *journal_info;/* journalling filesystem info */
};


5.内核分配进程描述符
struct thread_info
2.6版本的内核栈底(对于向下增长的栈来说)或者栈顶(对于向上增长的栈来说)放的是thread_info结构,通过thread_info里的task指针可以找到task_struct
struct thread_info{
    struct task_struct *task;
    struct exec_domain *exec_domain;
    _u32    flags;
    _u32    status;
    _u32    cpu;
    int        preempt_count;
    mm_segment_t addr_limit;
    struct restart_block     restart_block;
    void    *sysenter_return;
    int        uaccess_err;
}图在P22

thread_info的存放:硬件体系不同,current宏不同,有些硬件体系结构有一个专门寄存器存放当前task_struct指针,例如PowerPC;有些利用栈指针esp,例如x86系统。
x86系统:

  1. struct thread_info *thread = current_thread_info();
  2.     struct task_struct *tsk = thread->task;

  3. /* how to get the current stack pointer from C */
  4. register unsigned long current_stack_pointer asm("esp") __used;

  5. /* how to get the thread information struct from C */
  6. static inline struct thread_info *current_thread_info(void)
  7. {
  8.     return (struct thread_info *)
  9.         (current_stack_pointer & ~(THREAD_SIZE - 1));/*把栈指针esp的后13个有效位屏蔽掉,栈是8K的时候;栈4K屏蔽12个有效位*/
  10. }


  11. /* how to get the current stack pointer from C */
  12. static inline unsigned long current_stack_pointer(void)
  13. {
  14.     unsigned long sp;
  15.     asm("mov sp,%0; ":"=r" (sp));
  16.     return sp;
  17. }
二、进程的生命周期

进程的各个状态之间的转化构成了进程的整个生命周期。

TASK_RUNNING:进程是可执行的;或者正在执行,或者在运行队列中等待。这是进程在用户空间中执行的唯一可能的状态;也可以应用到内核空间中正在执行的进程。
TASK_INTERRUPTIBLE:条件和信号=》运行;TASK_UNINTERRUPTIBLE:条件=》运行
_TASK_TRACED:被其他进程跟踪的进程;
_TASK_STOPPED:进程停止执行;例调试期间接受任何信号都进入这种状态

设置当前进程状态:
set_task_state(task,state);<=>set_task_state(current,state);  

进程上下文:可执行文件载入到用户空间的进程地址空间执行,当程序执行了系统调用或者触发了某个异常(对内核有明确的接口),则程序陷入内核空间,称内核“代表进程执行”并处于进程上下文中,除非此间有更高优先级的进程需执行并由调度器做出调整,否则内核退出时,程序恢复在用户空间中继续执行。

进程家族树:父进程,子进程,兄弟
获取任务队列(双向循环链表)的下个进程:宏#define next_task(task)  list_entry(task->tasks.next,struct task_struct, tasks)
获取任务队列(双向循环链表)的上个进程:宏#define prev_task(task)  list_entry(task->tasks.prev,struct task_struct, tasks)
遍历整个任务队列:for_each_process(task)

三、进程的创建

Linux中创建进程与其他系统有个主要区别,Linux中创建进程分2步:fork()和exec()。

fork: 通过拷贝当前进程创建一个子进程,区别在于:PID、PPID、某些资源和统计量

exec: 读取可执行文件,将其载入到内存中运行

创建的流程:
        fork(),vfork(),_clone()库函数根据各自需要的参数标志调用clone(),由clone()调用do_fork()。
        do_fork()创建大部分工作,
        copy_process()函数的工作:

  1. 调用dup_task_struct()为新进程分配内核栈,task_struct、内核栈、thread_info等,其中的内容与父进程相同。
  2. check新进程(进程数目是否超出上限等)
  3. 清理新进程的信息(比如PID置0等),使之与父进程区别开。
  4. 新进程状态置为 TASK_UNINTERRUPTIBLE
  5. copy_process()调用copy_flags()更新task_struct的flags成员。
  6. 调用alloc_pid()为新进程分配一个有效的PID
  7. 根据clone()的参数标志,copy_process()拷贝或共享相应的信息
  8. copy_process()做一些扫尾工作并返回新进程指针
        再回到do_fork()。
        子进程优先执行,避免写时拷贝额外开销。

创建进程的fork()函数实际上最终是调用clone()函数。

写时拷贝(copy-on-write)
    在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
      那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?
      在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个,以只读方式共享。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。       在网上看到还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制拷贝大量不使用的数据,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
    页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。

    现在有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。

1.      现在P1用fork()函数为进程创建一个子进程P2,

内核:

(1)复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。

(2)为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。

 

2.       写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

 

 

3.       vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间

 

通过以上的分析,相信大家对进程有个深入的认识,它是怎么一层层体现出自己来的,进程是一个主体,那么它就有灵魂与身体,系统必须为实现它创建相应的实体, 灵魂实体与物理实体。这两者在系统中都有相应的数据结构表示,物理实体更是体现了它的物理意义。
PS:实际上COW技术不仅仅在Linux进程上有应用,其他例如C++的String在有的IDE环境下也支持COW技术,即例如:

string str1 = "hello world"; string str2 = str1;

之后执行代码:

str1[1]='q';
str2[1]='w';

在开始的两个语句后,str1str2存放数据的地址是一样的,而在修改内容后,str1的地址发生了变化,而str2的地址还是原来的,这就是C++中的COW技术的应用,不过VS2005似乎已经不支持COW。


    参考文献:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

vfork:不拷贝父进程的页表项,
1.调用copy_process()时,task_struct的vfork_done成员被设置为NULL
2.在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特定的地址
3.子进程先执行,父进程等待,知道子进程通过vfork_done指针像父进程发送信号,父进程才恢复执行
4.调用mm_release()时,该函数用于进程退出内存地址空间,并检查vfork_done是否为空,如果不为空,则会向父进程发送信号。
5.回到do_fork(),父进程醒来并返回。

总结:fork()与vfock()都是创建一个进程,但它们有以下三点区别: 
1.  fork  ():子进程拷贝父进程的数据段,代码段 
    vfork ( ):子进程与父进程共享数据段 
2.  fork ()父子进程的执行次序不确定 
    vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec
     或exit 之后父进程才可能被调度运行。 
3.  vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在
   调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。 


参考文献:http://blog.csdn.net/jianchi88/article/details/6985326

创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。

比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)

创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)
参考文献:http://www.cnblogs.com/wanghetao/archive/2011/11/06/2237931.html

在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。可以用于内核后台执行的操作。

这与之前提到的Linux内核是个单内核有关。
内核线程只能由内核线程创建。通过struct task_struct *kthread_create(int (*threadfn)(void *data), void *data,const char namefmt[],……)创建,通过wake_up_process()唤醒运行。或通过struct task_struct *kthread_run(int(*threadfn) (void *data),void *data,const char namefmt[],……)
调用do_exit()或int kthread_stop(struct task_struct *struct)结束线程。

四、进程的终止

和创建进程一样,终结一个进程同样有很多步骤: 

子进程上的操作(do_exit)

  1. 设置task_struct中的标识成员设置为PF_EXITING
  2. 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
  3. 如果BSD进程记账功能开启,条用acct_update_integrals()输出记账信息
  4. 调用exit_mm()释放进程占用的mm_struct
  5. 调用sem__exit(),使进程离开等待IPC信号的队列
  6. 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
  7. 把task_struct的exit_code设置为进程的返回值
  8. 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
  9. 调用schedule()切换到新进程继续执行

子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放掉了,但是它本身占用的内存还没有释放,
比如创建时分配的内核栈,task_struct结构等,用于向父进程提供信息。这些由父进程来释放。do_exit()永不返回。

父进程上的操作(release_task)
    1.调用_exit_signal(),继而调用_unhash_process(),后者又调用detach_pid()从pidhash中删除该进程,同时也要从任务列表中删除该进程。
    2._exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
    3.若进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。
    4.release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放tast_struct所占的slab高速缓存。

父进程受到子进程发送的exit_notify()信号后,并且内核不关注子进程信息,则将该子进程的进程描述符和所有进程独享的资源全部删除。

孤儿进程的寻父过程P32

从上面的步骤可以看出,必须要确保每个子进程都有父进程,如果父进程在子进程结束之前就已经结束了会怎么样呢?

子进程在调用exit_notify()时已经考虑到了这点。

如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。

find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)

阅读(635) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~