Linux内核之进程管理

进程描述符及任务队列

内核把进程放在叫做任务队列的双向循环链表中,链表中的每一项都是类型为task_struct的进程描述符。进程描述符包含的数据可以完整的描述一个正在执行的程序:打开的文件、进程的地址空间、挂起信号、进程状态等

进程描述符的存放

内核通过PID标识每个进程,pid_t pid其中的pid_tint类型,为了与老版本兼容,限制其大小为32767short int的最大值。我们可以自己修改pid的最大值来提高上限。

在内核中,访问任务通过指向该task_structa的指针,这是通过current 宏实现的。基于硬件的不同,该宏的实现也不同。有的体系可以专门拿出一个寄存器来存放指向当前进程的指针,用于加快访问速度。而像x86这样的体系结构,其寄存器不多,只能在内核栈的尾端创建thread_info结构,通过计算偏移量来间接得到当前的task_struct

进程状态

  • TASK_RUNNING(运行):进程是可执行的状态(运行或者就绪态都算)
  • TASK_INTERRUPTIBLE(可中断):进程正被阻塞,等待某些情况达成,内核会把进程设置为运行。接收到信号也会被提前唤醒并投入运行
  • TASK_UNINTERRUPTIBLE(不可中断):接收到信号也不会被唤醒
  • TASK_ZOMBIE(僵死):该进程结束,但是父进程没有调用wait系统调用回收资源。该进程描述符仍然保留,资源未被释放
  • TASK_STOP PED(停止):进程停止运行,常发生在接收SIGSTOP SIGSTP SIGTTIN SIGTTOU等信号的时候,调试期间接收任何信号也会

设置当前进程状态

内核经常会调整某个进程的状态,会使用set_task_state(task, state)函数,将指定进程设置为指定状态

进程上下文

进程创建

  • 其他操作系统:提供产生进程的机制,在新的地址空间创建进程,读入可执行文件,然后执行
  • Unix:将上述步骤分成forkexec来执行
    • fork通过拷贝当前进程创建一个子进程,子进程和父进程PID PPID 某些资源的统计量(比如挂起的信号)不一样
    • exec函数负责读取可执行文件并将其载入地址空间开始运行

写时拷贝

传统的fork直接拷贝所有资源给子进程,着过于简单且效率低下,慢且浪费资源

Linuxfork使用写时拷贝实现,写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核并不复制整个地址空间,而是父子进程共享同一个拷贝,只有在需要写入的时候,才会复制(在此之前是以只读方式共享)。

通常来说,我们fork得到的子进程都会直接执行另一个程序,所以实在没有必要再将父进程的所有数据复制到子进程,因为这些数据很大可能就没有被使用。比如fork之后立即调用exec,那么就无需复制操作了。这样子fork的实际开销就是复制父进程的页表以及给子进程创建task_struct

fork()

Linux通过调用clone系统调用来实现forkclone通过一系列参数标志来指明父子进程需要共享的资源。forkvfork_clone库函数都根据各自需要的参数来调用clone,再由clone去调用do_fork

do_fork完成了创建进程的大部分工作,该函数调用copy_process()函数,然后让进程开始运行

copy_process()函数的工作

  • 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构、task_struct,这些值和当前进程值相同,父子进程的描述符是完全相同的
  • 检查新创建的子进程,当前用户所拥有的进程数目有没有超过资源限制
  • 为使父子进程区分开, 进程描述符许多成员需被清0
  • 子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行
  • 调用copy_flags()以更新task_structflags成员,
  • 调用get_pid()为新进程获取一个PID
  • 根据传递给clone()的参数,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等
  • 让父进程和子进程平分剩余的时间片
  • 做完最后的处理工作后,返回一个指向子进程的指针

如果copy_process()函数成功返回,新创建的子进程被唤醒并被投入运行。内核一般选择子进程先执行,因为子进程一般会调用exec()函数,这样可以避免拷贝操作。而父进程有可能会调用写操作,造成写时拷贝操作。

线程在Linux中的实现

其他的操作系统,比如Microsoft Windows在内核中提供了专门支持线程的机制,但是Linux没有。从内核角度,Linux中没有线程这个概念。Linux将线程当作进程来实现,并没有使用特殊的调度算法或定义特别的数据结构来表示线程,线程仅仅被视为一个使用某些共享资源的进程。每个线程都有唯一的task_struct

对于Linux来说,这只是一种用来进程间资源共享的手段。

假设有一个包含四个线程的进程

  • 支持线程的系统:会有一个包含指向四个线程的指针的进程描述符,该描述符描述像地址空间,打开的文件这样的共享资源。线程本身再去描述它独占的资源
  • Linux:它仅仅创建四个进程并分配四个普通的task_struct结构,建立这四个进程时指定它们共享某些资源

内核线程

  • 内核需要在后台执行一些任务,这种任务通过内核线程实现——在内核运行的标准进程
  • 内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它们只在内核空间运行,不会切换到用户空间
  • 一般情况下,内核线程会将它在创建时得到的函数永远执行下去,该函数通常由一个循环构成,在需要的时候,这个内核线程会被唤醒,执行完毕则自行休眠

进程终结

当一个进程终结时,内核必须释放它所占有的资源并通知其父进程

  • 一般来说,进程的析构发生在它调用exit()函数之后
  • exit()函数可以显示调用也可以隐式调用(C语言编译器会在main()函数的返回点后面放置调用exit()的代码)
  • 当进程收到不能处理也不能忽略的信号后,也会被动终结

进程的终结大部分靠调用do_exit()函数来完成

至此,与进程相关的资源都被释放掉了,进程不可运行并处于TASK_ZOMBIE状态。它所占用的所有资源就是保存thread_info的内核栈和保存task_struct结构的那一小片slab。此时进程存在的唯一目的就是向它的父进程提供信息

删除进程描述符

在调用了do_exit()之后,尽管线程已经僵死不能运行,但是系统还是保留了它的进程描述符。不一次性处理完是为了让系统在子进程终结后仍能得到它的信息。因此,进程终结的清理工作和进程描述符被删除是分开执行的。

wait()这一族函数都是通过唯一的系统调用wait4()实现的,它将挂起调用它的进程,直到其中的一个子进程推出。此时函数会返回该子进程的PID

孤儿进程

如果父进程在子进程退出之前退出,必须有机制来保证子进程找到一个新的父亲,否则这些孤儿进程就会在退出时永远处于僵死状态,这样会浪费内存(没有父进程调用wait4()函数回收资源)。

我们让init做它们的父进程,在do_exit()中会调用notify_present(),该函数会通过forget_original_parent来执行寻父过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值