当前位置:   article > 正文

Linux进程详解 【Linux由基础到进阶】_linux 进程

linux 进程

进程的概念:

进程:处于执行期的程序和它包含的资源的总合。

在现代操作系统中,进程提供两种虚拟机制:虚拟处理器虚拟内存

虚拟处理器:

实际上许多进程在共享处理器,而虚拟处理器可以给每个进程一种假象,自己在独享在独享处理器。

虚拟内存:

虚拟内存让每个进程在获取和使用内存资源时,让自己觉得自己拥有整个内存资源。

进程的产生

在现代Linux系统中,通常是调用fork()系统调用。
该系统调用通过复制一个现有进程来创建一个全新的进程,
调用fork()的通常称为父进程,新产生的进程称为子进程,
在调用结束后,在返回点这个地方,父进程恢复执行,子进程开始执行。通常创建新进程都是为了执行新的不同的程序,接着调用exec*()这族函数就可以创建新的地址空间,把新程序载入。

最终,程序通过exit()系统调用退出执行,这个函数会终结进程并将其占用的资源释放掉,进程可以通过wait4()系统调用查询子进程是否终结,这使得进程拥有了等待特定进程执行结束的能力,进程在退出执行后被设置成僵死状态,直到它的父进程调用wait()或者是waitpid()为止。

进程的管理

进程描述符

内核通过一个双向链表来管理进程,链表的每一项都是类型为task_struct(进程描述符)
下面为Liunx的源码中关于task_struct的结构
在这里插入图片描述
在这里插入图片描述

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样就能达到对象复用和缓存着色的问题。

进程描述符的存放

内核通过一个唯一的标识符PID来标识每个进程,PID是一个数,表示为pid_t隐含类型(实际为Int类型)

为了和老版的Linux和Unix兼容,

 PID默认的最大值为32768(short Int 的最大值)
 这个就是系统内允许同时存在的最大进程数。
  • 1
  • 2

但是大型服务器可能需要更多进程, 系统管理员可以通过

 /proc/sys/kernel/pid_max
  • 1

来设置上限

在内核中,访问任务通常需要获得其指的task_struct指针,实际上,内核中的大部分处理进程的代码都是通过task_struct进行的,因此,通过current宏(一个全局变量,存的是当前PCB的地址)查找到当前正在运行进程的进程描述符的速度就尤为重要。
硬件体系不同,该宏的实现也不同。
当寄存器比较富余的硬件体系结构,就可以拿专门的寄存器来存放指向当前进程的task_struct指针。
而像x86这样的体系,只能在内核栈的尾端创建thread_info结构,通过通过计算偏移间接的查找task_struct结构

进程的状态

TASK_RUNNING运行进程是可执行的,或者正在执行,或者在运行队列中等待执行
TASK_INTERRUPTIBLE可中断进程正在睡眠(被阻塞),等待内核将它设置为运行,也可以因为接收到信号,而提前被唤醒并投入运行
TASK_UNINTERRUPTIBLE不可中断除了不会接收到信号而提前被唤醒,其他的都与可中断一样
TASK_ZOMBIE僵死该进程已经结束了,但是父进程还没有调用wait4()系统调用,该进程的进程描述符还被保留着
TASK_STOPPED停止进程停止运行,进程没有投入运行也不能投入运行,通常发生在接收到 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号

ps ax 查看进程状态
在这里插入图片描述
各状态符的含义

STAT代码说明
S睡眠。通常是在等待某个事件的发生,如一个信号或有输入可用
R运行。严格来说,应是“可运行”,即在运行队列中,处于正在执行或即将运行状态
D不可中断的睡眠(等待)。通常是在等待输入或输出完成
T停止。通常是被shell作业控制所停止,或者进程正处于调试器的控制之下
Z死(Defunct)进程或僵尸(zombie)进程
N低优先级任务,nice
W分页。(不适用于2.6版本开始的Linux内核)
S进程是会话期首进程
+进程属于前台进程组
1进程是多线程的
<高优先级任务

在这里插入图片描述

设置进程状态

内核通常要调整某个进程的状态,通常由这个函数完成

 set_task_state(task,state);  /*将任务 'task’的状态设置为 'state'* /
  • 1

进程上下文

可执行代码是进程的重要部分,这些代码从可执行文件载入到进程的地址空间中执行,一般程序在用户空间中执行,当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。

系统调用与库函数的区别

系统在用户空间进程和硬件设备之间添加了一个中间层(系统调用),为了和用户空间上运行的进程进行交互,内核提供了一组接口,通过该接口,应用程序可以访问硬件设备和其他操作系统资源,这组接口就像一个使者,传递着应用程序的请求,而内核满足该请求。

那为什么要有这个中间层呐?

 1.  它为用户空间提供了一种硬件的抽象接口
  • 1

当需要对硬件进行操作时,就可以不用管磁盘的类型和介质,如果说要操作一个文件的话,就不用管该文件所在的文件系统是那种类型。

 2.系统调用保证了系统的稳定和安全
  • 1

有了中间层的话,就可以避免应用程序不正确的使用硬件设备,窃取其他进程的资源,或做出危害系统的事情。

 3.每个进程都运行在虚拟空间中,如果没有这个中间层的话,应用程序可以随意访问硬件而内核又对此一无所知的话,就没办法实现多任务和虚拟内存。
 4.系统调用开销大,效率低。
  • 1
  • 2

每次在执行系统调用时,Linux必须从运行用户代码切换到内核代码,然后再返回用户代码,我们通常的办法是,尽量减少系统调用,并且让每次系统调用执行更多的工作,了例如每次读写大量的数据,而不是一俩个数据。

库函数:如printf strlen等,
有些库函数是需要系统调用来实现的
如:printf (底层调用了系统调用write())
有些库函数是不需要系统调用来实现的
如:strlen (这个程序员自己就能实现)
在这里插入图片描述

下面的图就是库函数,系统调用,内核,硬件的关系。

库函数通过调用系统调用来通知内核对硬件进行一些操作。
在这里插入图片描述

进程家族树

Linux系统的进程之间存在一个很明显的继承关系,所有的进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。
系统的每一个进程必有一个父进程,相应的,每个进程都拥有零个或多个子进程,拥有相同父进程的进程互称为兄弟。

在Linux中,所有命令的父进程都是bash,
在这里插入图片描述
我们执行两次ps -f 我们发现两次ps的进程的父进程都是2460,而2460为bash的进程号。
在这里插入图片描述
在执行命令时,bash先复制自己,生成子bash,然后再对其进行替换,生成新进程ps。

进程创建

总述:Liunx进程创建与其他操作系统不同,其他操作系统都提供了产生进程的机制,而Linux把上述步骤分解到两个单独的函数中执行

   1. fork
   2. exec()族函数 (execve(),execlp(),execle(),execv(),execvp())
  • 1
  • 2

fork通过当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID(进程号),PPID(父进程号),某些资源和统计量(例如挂起的信号,等等)
exec()函数负责读取可执行文件并将其载入地址空间并开始运行。

写时拷贝技术

:一种推迟复制甚至免除拷贝的技术
当子进程进行复制时,子进程并不复制父进程的全部空间,而是与父进程共享一块空间,只要在需要写入时,数据才会被复制。
在这里插入图片描述
只有当子进程要修改内容时,才会给要修改的该页重新分配一页,但此时 4,8页都是子进程公用父进程的
在这里插入图片描述
为什么要有写时拷贝技术呐?

我们之前说过,进程的创建分为两步,第一步是复制进程,第二步是对其进行替换。
因为在复制的时候,谁也不知道复制出来的进程是直接使用还是要用来替换。
假如说要用来替换的话,那么之前刚刚费劲千辛万苦复制的父进程的资源,现在又直接要进行释放,那么这不是很可惜吗?所以我们可以通过这种技术,来避免这种资源的消耗。
而假如说是要直接使用的话,那么子进程和父进程同时共享一块空间,那么这样也不是节省了空间的使用吗?

复制进程的过程

fork(),vfork(),_clone()都是直接调用调用do_fork() (随着Linux版本的迭代,具体的实现也会变化)

1 :调用get_pid为新进程获取一个pid

 假如说先执行其他的,而最后连pid都获取不了,那之前的操作不就什么意义都没有
 了吗?
  • 1
  • 2

2:调用dup_task_strut()为新进程创建一个内核栈,thread_info结构和task_struct

 这些值与当前进程值相同,此时,子进程与父进程的描述符是完全相同的。
  • 1

3:检查新创建的这个子进程,当前用户所拥有进程数有没有超过限制
4:子进程中进程描述符中的许多成员被置为初始值(为了与父进程区分开)
5:子进程的状态被设置为TASK_UNINTERRUPTIBLE

 保证该进程不会运行,因为此时进程还没有创建完
  • 1

6:copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。

7:根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。

8:父进程与子进程平分剩余的时间片
9:回到do_fork函数,如果copy_process()成功返回,新创建的子进程被唤醒并投入运行,一般子进程都会马上调用exec()函数,防止父进程写入从而写时拷贝造成额外开销。

进程的结束

进程的结束一般在它调用exit()之后(显示调用),从某个函数的主程序返回后会隐式的调用exit()

该终结过程大多由do_exit()实现

1:首先,将task_struct中的标志成员设置为PF_EXITING。
2:其次,调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保
没有定时器在排队,也没有定时器处理程序在运行。
3:如果BSD的进程计账功能是开启的,do_exit()调用acct_process()来输出计账
信息。
4:然后调用_exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它
们(也就是说,它们没被共享),就彻底释放它们。
5:接下来调用exit__sem()函数。如果进程排队等候IPC信号,它则离开队列。
6:调用_exit_files()、_exit_fs()、exit_namespace()和exit_sighand(),以
分别递减文件描述符、文件系统数据,进程名字空间和信号处理函数的引用计数。如果其中某些引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
7:接着把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供
的代码中,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供
父进程随时检索
8:调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中
的其他线程或init进程,并把进程状态设成TASK_ZOMBIE。
9:·最后,do_exit()调用schedule()切换到其他进程(参看第4章)。因为处于
TASK_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

僵死进程

链接: 请跳转这篇博客,我在这篇博客中讲了

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/635152
推荐阅读
相关标签
  

闽ICP备14008679号