进程_Linux内核设计与实现笔记

进程

进程是处于执行期的程序以及相关资源的总称,是正在执行的代码的实时结果.

进程部分位于Kernel的PM层.进程是Unix操作系统的抽象概念中最基本的一种,操作系统的存在就是为了运行用户程序,所以进程管理是所有操作系统的心脏所在.
程序本身不是进程,进程不局限于可执行程序代码段(text section),完全可能出现多个不同的进程,执行的是同一个程序, 多个进程之间还可以共享一些资源,如打开的文件,地址空间等.

线程

执行线程简称线程,是进程中的活动对象,每个线程都具有独立的程序计数器,进程栈,和一组进程寄存器.

调度对象是进程中的活动对象线程,而不是进程本身,通常一个进程包含多个线程,即多线程序.

虚拟处理器与虚拟内存

进程提供两种机制,虚拟处理器虚拟内存,实际可能是许多进程分享同一个处理器,虚拟处理器会让进程觉得自己在独享处理器,虚拟内存同样,会让进程觉得自己拥有整个系统的内存资源.

包含在同一个进程中的线程之间可以共享虚拟内存.但是每个线程都拥有各自的虚拟处理器.

从内核的角度说,没有线程的概念,对Linux来说线程只不过是进程之间共享资源的一种手段.

创建进程

使用fork函数创建进程

...
NAME
   fork - create a child process

SYNOPSIS
   #include <sys/types.h>
   #include <unistd.h>

   pid_t fork(void);

DESCRIPTION
   fork()  creates  a new process by duplicating the calling process.  The new process is
   referred to as the child process.  The calling process is referred to  as  the  parent
   process.

   The  child  process and the parent process run in separate memory spaces.  At the time
   of fork() both memory spaces have the same  content.   Memory  writes,  file  mappings
   (mmap(2)),  and unmappings (munmap(2)) performed by one of the processes do not affect
   the other.
   ...
   RETURN VALUE
   On  success, the PID of the child process is returned in the parent, and 0 is returned
   in the child.  On failure, -1 is returned in the parent, no child process is  created,
   and errno is set appropriately
   ...

fork函数  
Creat thread   
child process may use all of father's resources  
pid_t fork(void);  
– 参数:无  
– 返回值:执行成功,返回子进程pid给父进程,0返回给子进程;出现错误,返回-1给父进程。执行失败的唯一情况是内存不够或者id号用尽,不过这种情况几乎很少发生。  
– 系统函数fork 调用成功,会创建一个新的进程,它几乎会调用差不多完全一样的fork 进程  
– 子进程的pid 和父进程不一样,是新分配的  
– 子进程的ppid(他爷爷的ID) 会设置为父进程的pid,也就是说子进程和父进程各自的
“父进程”不一样.  
– 子进程中的资源统计信息会清零.  
– 挂起的信号会被清除,也不会被继承.  
– 所有文件锁也不会被子进程继承.  

进程在创建它的时刻开始存活,Linux中,通常使用fork()系统调用来创建进程,该系统调用通过复制一个现有的进程来创建一个全新的进程,调用fork()的进程的进程称为父进程,产生的新进程称为子进程,调用结束后,返回到父进程中,父进程继续向下执行, 子进程开始执行,

父进程调用子进程,然后自己向下执行,同时子进程被调用后也开始执行.
fork系统调用从内核返回两次,一次返回到父进程,另一次返回到新产生的子进程.

通常,创建新的进程都是为了立即执行新的程序,使用exec函数簇可以创建新的地址空间,并将程序(可执行文件)载入; fork函数实际上是由clone系统调用实现的.

clone()的所有参数定义在 linux/sched.h 中.

调用关系

创建进程 -> fork() -> clone() -> do_fork() -> copy_process()

copy_process()函数完成

  1. 调用dup_task_struct() 创建内核栈,创建thread_info结构和task_struct结构.
  2. 检查并确保当前进程数目没有超出限制.
  3. 子进程使自己与父进程能够区分开来.进程描述符中的许多成员(统计信息等)都将被设置为初始值.
  4. 子进程的状态设置为TASK_UNINTERRUPTIBLE,保证它不可投入运行.
  5. copy_process调用copy_flags(),更新task_struct的flags成员.
  6. 调用alloc_pid() 为新进程分配一个有效的PID.
  7. 传递clone的参数标志,copy_process拷贝或共享打开的文件.
  8. 扫尾工作,返回一个指向子进程的指针 .

回到do_fork函数,如果copy_process函数返回成功,那么内核会有意让子进程先执行,一般子进程会马上调用exec这样会避免写时拷贝的开销.

进程描述符

进程的另一个名字是任务task,从内核开发者的角度所说的任务,通常为从内核出发看到的所有进程,内核把进程列表放在叫做任务队列的双向循环链表中,其中的每一项的类型都为 task_struct,被称为进程描述符(process descriptor)的结构,该结构定义在<linux/sched.h>文件中,进程描述符中包含一个具体的进程信息.

/*进程描述符task_struct结构*/
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
     * For reasons of header soup (see current_thread_info()), this
     * must be the first element of task_struct.
     */
    struct thread_info      thread_info;
#endif
    /* -1 unrunnable, 0 runnable, >0 stopped: */
    volatile long           state;

    /*
     * This begins the randomizable portion of task_struct. Only
     * scheduling-critical items should be added above here.
     */
    randomized_struct_fields_start

    void                *stack;
    atomic_t            usage;
    /* Per task flags (PF_*), defined further below: */
    unsigned int            flags;
    unsigned int            ptrace;
    /*
     * WARNING: on x86, 'thread_struct' contains a variable-sized
     * structure.  It *MUST* be at the end of 'task_struct'.
     *
     * Do not put anything below here!
     */
};

task_struct相对较大,因为该结构中包含了内核管理一个进程需的所有信息,进程描述符中包含的数据能够完整地描述一个正在执行的程序,进程描述符主要包括:

  • 该进程打开的文件,
  • 进程的地址空间,
  • 挂起的信号,
  • 进程的状态等.

分配进程描述符

Linux通过slab分配task_struct结构,这样能够达到对象复用缓存着色(cache coloring).使用slab动态生成task_struct结构,只需在栈底或栈顶创建一个新的结构struct thread_info.

x86 平台上struct thread_info文件在 <asm/thread_info.h>中定义

current获得当前正在运行程序的进程描述符

在内核中访问任务通常需要获得指向他的task_struct的指针,内核中绝大多数处理进程的代码都是通过task_struct进行的,可以通过current宏查找当前正在运行的进程的进程描述符.

这个宏根据硬件体系结构的不同,实现也不同1:
有的硬件结构可以使用一个寄存器来专门存放task_struct结构的地址,这样current的实现,就会非常容易,直接返回该寄存器的值就行了,但是有的硬件体系结构的寄存器较少,所以只能通过创建一个thread_info结构,通过计算地址偏移间接查找task_struct结构.

current宏实现的两种方法:

  1. 通过 thread_info结构体间接查找进程描述符.
  2. 通过 直接返回专门存放进程描述符的寄存器的值找到进程描述符.

thread_info结构体

头文件路径:

.\linux源码目录\arch\arm\include\asm\thread_info.h
/*
 * low level task data that entry.S needs immediate access to.
 * __switch_to() assumes cpu_context follows immediately after cpu_domain.
 */
struct thread_info {
    unsigned long       flags;      /* low level flags */
    int         preempt_count;  /* 0 => preemptable, <0 => bug */
    mm_segment_t        addr_limit; /* address limit */

    struct task_struct  *task;      /* main task structure */
    __u32           cpu;        /* cpu */
    __u32           cpu_domain; /* cpu domain */
    struct cpu_context_save cpu_context;    /* cpu context */
    __u32           syscall;    /* syscall number */
    __u8            used_cp[16];    /* thread used copro */
    unsigned long       tp_value[2];    /* TLS registers */
#ifdef CONFIG_CRUNCH
    struct crunch_state crunchstate;
#endif
    union fp_state      fpstate __attribute__((aligned(8)));
    union vfp_state     vfpstate;
#ifdef CONFIG_ARM_THUMBEE
    unsigned long       thumbee_state;  /* ThumbEE Handler Base register */
#endif
};

每个任务的thread_info结构在他的内核栈尾端创建。结构中task域中存放的是指向该任务实际task_struct的指针.通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗.寄存器较弱的体系结构不是引入thread_info结构的唯一原因,这个新建的结构使在汇编代码中计算其编译变得更加容易.

PID的存放

内核通过一个唯一的进程标识值(process identification value)即PID来表示每个进程。PID是一个数,表示为pid_t隐含类型2,实际上就是一个int类型。为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768(short int短整型的最大值),这个限值定义在<linux/threads>中。内核把每个进程的PID存放在它们各自的进程描述符中.

这个PID限值(允许同时存在的进程的最大数目)越小,转一圈就越快(进程轮回执行一圈),通常数值大的进程在数值小的进程之后运行(先运行的程序具有更小的PID值,因为PID值递增分配),如果需要修改pid限值,可以通过修改/proc/sys/kernel/pid_max提高上限

state域

进程描述符中的state域描述了进程的当前状态,系统中的每个进程都必然处于五种进程状态中的一种:

  1. TASK_RUNNING 运行:进程正在执行或等待执行.
  2. TASK_INTERRUPTIBLE 可中断 : 进程睡眠被阻塞,等待条件达成被唤醒,状态转为运行.
  3. TASK_UNINTERRUPTIBLE 不可中断3:就算接收到信号也不会被唤醒.
  4. __TASK_TRACED 被其他进程跟踪的进程,如通过ptrace对调试的程序进行跟踪.
  5. __TASK_STOPPED 进程停止执行4,进程没有投入运行.

设置当前的进程状态

调整进程状态的函数

set_task_state(task,state)/*将任务task的状态设置为state*/  

该函数将指定的进程设置为指定的状态,在SMP系统中,会设置内存屏障来强制其他处理器作重新排序 ,否则等价于

task->state = state;/*设置状态位的状态为state*/

set_current_state(state)和set_task_state(current,state)含义相同,具体在头文件 <linux/sched.h>中.

NOTE: task域位于thread_info结构中,为指向进程描述符task_struct结构的指针.

fork函数与vfork函数的区别

fork函数会拷贝父进程的页表项到子进程,页表项还没有写时拷贝.
vfork()函数不会拷贝父进程的页表项,

fork 拷贝当前进程,创建一个子进程
vfork 同fork 但是 父进程会等待子进程退出后在继续执行
子进程做为父进程的一个单独的线程在他的地址空间中运行,父进程被阻塞,直到子进程退出

页表项

操作系统为了方便管理内存地址,将内存地址分为了很多个页,每个页的地址,即为一个页表项.

内核线程

内核线程是独立运行在内核空间的标准进程,与普通进程的区别是它没有独立的地址空间,5所有的内核线程都运行在同一个地址空间范围内.

查看内核线程命令:

ps -ef 

创建内核线程

linux/kthread.h 中提供了从现有内核线程中创建一个新的内核线程的方法
---kthread_create函数,新创建的进程处于不可运行的状态,如果不通过调用wake_up_process明确唤醒它,他就不会运行.

通过kthread_run可以实现创建一个进程并唤醒它,这是对上面两个函数的封装,

#define kthread_run(threadfn, data, namefmt, ...)              \
({                                     \
    struct task_struct *__k                        \
        = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                          \
        wake_up_process(__k);                      \
    __k;                                   \
})

为了提高代码的运行效率,内核中很多函数采用了宏来封装,这减掉了函数调用所需要的时间开销.

终结内核线程

内核线程启动后,就会一直运行直到调用do_exit()退出,或者内核中的其他部分调用 kthread_stop 退出 ,进程终结时,内核必须释放他的所有资源,并告知其父进程,进程的析构一般由自身引起,它显示或隐式的调用exit系统调用,终结自己,C编译器会在主函数的返回点后面放置exit系统调用代码实现隐式调用exit,或者进程发生异常,被动终结.

无论进程如何终结,该任务都会由do_exit系统调用完成6.

do_eixt完成的工作

  1. 将task_struct中的标志成员设置为PF_EXITING.(设置进程描述符中的flags为关闭标志)
  2. 调用del_timer_sync()删除任一内核定时器,确保没有定时器在排队,也没有定时器处理程序在运行.
  3. 如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals(),输出记账信息
  4. 调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用他们(说明内存没有被共享),就彻底释放他们.
  5. 调用sem__exit()函数,如果进程在排队等候IPC信号,那么就离开队列
  6. 调用exit_files()和exit_fs(),以分别递减文件描述符和文件系统的引用计数,如果引用计数降为0,说明该进程没有使用相应资源,可以释放.
  7. 将存放在task_struct中的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他内核机制规定的退出动作,退出代码存放在这里供父进程随时检索.
  8. 调用exit_notify通知父进程,给子进程找养父,(父进程要终结,那么他必须为他下面的子进程,找到养父,否则他下面的子进程就都成为了孤儿进程),养父可以是老祖宗Init也可以是其他进程.并将进程状态task_struct结构中的exit_state设置为EXIT_ZOMBLE.
  9. do_exit调用schedule切换到新的进程(调用调度程序,放弃处理器时间,让调度程序去调度其他进程),因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码,do_exit永不返回.

    flags标志定义在<linux\sched.h>中.

    /*
    * Per process flags
    */
    #define PF_IDLE         0x00000002  /* I am an IDLE thread */
    #define PF_EXITING      0x00000004  /* Getting shut down */
    /*...*/

如果进程而资源没有共享,那么所有的相关资源都被释放掉了,并处于EXIT_ZOMBIE退出状态,它占用的所有内存就是内核栈,thread_info结构和task_struct结构,
此时该进程存在的唯一目的,是为了向父进程提供信息,父进程检索到这些信息后,确认他已经死亡,为它做善后处理,该进程所持有的剩余内存全部归还给系统使用.

do_exit之后,父进程为为它做善后处理之前,尽管进程已经僵死不能在运行,但是系统还是保留了他的进程描述符,这样做可以让系统有办法在子进程终结后仍然能够获取他的信息,即进程终结时的清理工作,与进程描述符的删除工作被分开执行, 直到父进程检索到已死亡的子进程的信息,或者通知内核它并不关注哪些信息后,子进程的task_struct结构才被释放

处理掉僵死进程

---释放进程描述符

父进程通过wiat4系统调用负责实现子进程的善后处理,他的标准动作:

  • 挂起调用它的进程,直到它的一个子进程退出,此时函数会返回该子进程的PID,
  • 调用该函数时提供的指针会包含子函数退出时的退出代码.
  • 最终释放进程描述符时,会调用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高速缓存.

至此进程描述符和所有进程独享的资源就全部释放掉了.

孤儿进程,进退维谷

父进程在子进程之前退出,那么必须给他找到养父,否则这些孤儿进程就会在退出时永远处于僵死状态,因为没有父进程来检索他们的死亡信息,并对他们进行善后处理(删除进程描述符).
这不但浪费系统资源,而且白白耗费内存,
解决方法是在他的父进程死亡时,给他在当前线程组内找一个进程给他找养父,不行就找老祖宗init进程.

do_exit中会调用exit_notify,该函数会调用forget_original_parent,而后者回调用find_new_reaper来执行寻父过程,这段CODE会在当前线程组内给他找养父,如果不行就找Init.


小结:

  • 每个线程都具有独立的程序计数器,进程栈,和一组进程寄存器.
  • 进程提供两种机制,虚拟处理器和虚拟内存,
  • 线程对于Kernel来说只是一种共享内存的手段
  • 创建进程,fork
  • 将可执行文件载入内存空间开始运行,exec
  • 获取进程描述符,current宏
  • current宏的实现根据硬件结构不同,有两种实现方法.
  • 进程描述符结构 task_struct
  • 每个进程的PID存放在各自的进程描述符中
  • 进程描述符中的state域描述了进程的当前状态,
  • 进程的五种状态
  • 设置进程的状态
  • 查看内核线程
  • 创建并唤醒内核线程 kthread_run
  • 终结内核线程 exit
  • 页表项

进程间的关系

进程之间有明显的继承关系,所有进程都是PID为1的init进程的后代,每个进程必有一个父进程和任意个子进程.

进程的存放和表示

进程间的关系存放在进程描述符task_struct中,每个task_struct都包含一个指向父进程的指针parent和一个指向子进程的指针children.

current宏

current宏用于查找当前正在运行进程的进程描述符task_struct,具体实现因机器结构而异.

进程的创建

创建进程的过程为在新的地址空间中创建进程,读入并执行可执行文件;在Linux中,将这个步骤分解到了两个函数中,fork负责拷贝当前进程做为一个子进程(创建一个新的进程),(fork没有读入可执行文件并执行的功能),在子进程(新的进程)中通过exec函数读入可执行文件并开始执行.

写时拷贝

如果子进程不需要父进程的资源,那么fork的拷贝父进程的资源就是白做工,所以有了写时拷贝,只在子进程需要的时候,才会去拷贝父进程,如果子进程直接调用exec函数,执行了可执行文件,并退出,那么就不需要拷贝父进程的资源了.

线程

在Linux中线程是一种特殊的进程,是共享资源的一种手段,同一个进程中可以包含多个线程,多个线程之间可以共享同一资源7.

wait系统调用

wait的作用是挂起调用它的进程,即进程一旦调用了 wait,就立即阻塞自己,等待它的一个子进程退出,wait就会收集这个子进程的信息并把它彻底销毁后返回,恢复正常状态.

父进程收集后代信息的手段

内核负责实现wait4系统调用,Linux通过C库通常提供(wait函数族) wait, waitpid, wait3, wait4 函数,这些函数有细微差别,但是都能够实现返回关于终止进程的状态.

父进程可以通过wait4系统调用查询子进程是否终结,这使得进程拥有了等待特定进程执行完毕的能力,进程退出执行后,将被设置为僵死状态,直到他的父进程调用wait(),或waitpid为止,才彻底结束.

通过阻塞自己直到它的子进程终结,wait函数将会为终结的子进程做善后处理(销毁,删除进程描述符,释放他的所有资源).

进程如何消亡

程序通过显示或隐式调用exit系统调用退出执行8,这个函数会终结进程,并将其占用的资源释放掉.此时该线程处于僵死状态,并且不能够运行. 需要使用wait函数族来对该进程实现销毁处理 .



  1. 如x86构架上current宏的实现.而在PPC中,有足够多的寄存器,并且因为访问进程描述符是一个重要的频繁操作,所以PPC内核开发者为进程描述符单独开设了一个寄存器,current宏只需要将存放task_struct结构地址的r2寄存器的值返回就行了.

  2. 隐含类型指数据类型的物理表示是未知或者不相关的.

  3. 该状态通常在进程必须在等待时不受干扰,或等待事件很快就会发生时出现;由于处于此状态的任务对信号不做响应,使用的较少,因为在该状态,进程接收到任何信号都不会做出响应,所以即使使用ps命令看到被标记为D但又不能杀死的原因,你发送SIGKILL信号,他也会无视掉.通常这种状态的进程,应该有重要的事没有处理完,甚至可能还持有信号量,所以不应该强制,想尽办法去杀死他.

  4. 这种状态通常发生在接收到SIGSTOP,SIGSTP,SGITTIN,SIGTTOU等信号时,此外在调试期间接收到任何信号,都会时进程进入这种状态.

  5. Linux是单内核操作系统.

  6. 虽然让人伤感,但进程终归是要终结的.

  7. Linux中的进程与线程的概念是很模糊的,不刻意区分线程与进程,线程只不过是一种共享资源的手段罢了.

  8. 显式或隐式调用;隐式调用:编译器会在main函数返回部分添加exit系统调用.

猜你喜欢

转载自www.cnblogs.com/lifo/p/11791605.html
今日推荐