文章标题

进程管理

看过了操作系统相关的书籍总是很容易忘记,偶然看了一部分Linux内核设计与实现终于找到了其中的原因:操作系统类书籍讲求大而全,蜻蜓点水。Linux相当于把它实例化,是我们切切实实可以看到的东西。本文记一下看书的知识点以及自己的观点。由于不做内核相关的开发,前两章节就先跳过。

进程管理

进程就是处于执行期的程序(目标码存放在某种存储介质上)。在执行的期间还包括了其他许多资源(文件,信号,内核内部数据,处理器状态,内存地址空间等)。

线程是进程中的活动对象。简单的讲进程包含线程,共享大多数资源。线程有独立的程序计数器,进程栈,寄存器。内核调度的对象时线程而不是进程。

在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。

进程由fork()创建,返回两次。为0返回给子进程,>0返回给父进程。实际上fork是由clone()调用实现。

进程由exit()执行退出。退出后释放所占用的资源,进入僵死状态。但是仍然有一部分资源需要父进程去释放,由父进程调用wait()函数进行处理。

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列(task_list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符process descriptor)的结构.

Linux通过slab分配器分配task_struct 结构,这样能达到对象复用和缓存着色(cache coloring).

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;
};

每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。

进程描述符的存放

说白了就是指针存放在哪里。pid_t类型,实际是int类型。为了兼容一般是32768大小。可以通过/proc/sys/kernel/pid_max来修改。内核通过current宏查找到具体位置。宏是体系架构相关的,X86系类寄存器比较少,放在内核栈最后为的thread_info中*task指针。其他直接使用寄存器。

进程的状态

需要注意的是在休眠时候有INTERRUPTIBLE和UNINTERRUPTIBLE之分。UNINTERRUPTIBLE会忽略信号,不能用信号杀死进程。

还有两种状态是调试相关的。

TASK-TRACED一一一被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。

_STOPPED〔停止)—进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP, SIGSTP,SIGTTIN, SIGTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

可以通过set_task_state(task,state) 改变进程状态

进程上下文

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行—对内核的所有访问都必须通过这些接口。

Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此。所有的进程都是PID为1 的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取初始化脚本,执行相关程序,完成系统启动。

进程的创建

Unix的进程创建和别的系统不同。一般的实现是创建进程,读入可执行文件,执行。Unix分为fork()和exec().首先,fork()通过拷贝当前进程创建一个子进程P子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID )和某些资源和统计量〔例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。

linux使用写时拷贝copy-on-write页实现口写时拷贝是一种可以推迟甚至免除拷贝数据的技术。资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。这比其他系统动辄复制的效率高很多。

fork()

Linux使用clone()去完成fork函数,通过调整参数标志去完成父进程与子进程所共享的资源。clone()->do_fork()->do_process().

  1. 调用dup_task_struct()为新进程创建一个内核栈、thread_info和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  2. 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
  3. 子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。task_struct中的大多数数据都依然未被修改。
  4. 子进程的状态被设置为TASK_UNINTERRUPITABLE,以保证它不会投入运行。
  5. copy_process()调用copy flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  6. 调用alloc_pid()为新进程分配一个有效的PID.
  7. 根据传递给clone()的参数标志,capy_process拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享:否则,这些资源对每个进程是不同的,因此被拷贝到这里。
  8. 最后,copy_process()做扫尾工作并返回一个指向子进程的指针。

线程在Linux中的实现

Linux实现线程的机制柞常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。

创建线程

创建线程的时候不同的就是clone()的时候传递的标志位不同。

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

fork():

clone(SIGCHLD, 0);

vfork():

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

内核线程

内核经常需要在后台执行一些操作。内核线程没有独立地址,运行在内核空间中。这种任务可以通过内核线程(Kernel thread)完成—独立运行在内核空间的标准进程。内核线程由kthreadd内核进程衍生出来。像flush,ksofirqd。

进程终结

进程终结时释放所有资源。通过调用exit()函数。C语言编译器在main()函数返回点后面放置exit()函数。

  1. 将task_struct中的标志成员设置为PF_EXITING.
  2. 调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。
  4. 4然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
  5. 接下来调用sem_exit()函数。如果进程排队等候IPC信号,则离开队列。
  6. 调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
  7. 接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
  8. 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。
  9. do_exit()调用schedule()切到新的进程。

删除进程描述符

在调用了do_exit()后,进程已经僵死了不在运行。此时还有进程描述符等资源还没有清除。它会通知父进程。

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

如果在子进程退出之前父进程就已经退出了。子进程会在当前线程组找一个线程作为父亲,如果不行就让init成为父亲。init会定时调用wait()函数清除僵死进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值