在linux里搞懂了进程,也就等于掌握了操作系统的基本功。

咱先聊个明白,在Linux里搞懂了进程,也就等于掌握了操作系统的基本功。这里边CPU、Ctrl键、Page页和RSS这些关键概念,一个都不能少。其实进程这东西,说白了就是正在跑的程序加上一堆它能用的资源,是操作系统分配东西的最小单位。像那些打开的文件、挂着的信号、内核里的内部数据、处理器状态、内存地址空间啥的,全都是进程的“家当”。Linux对线程和进程没做硬区分,线程说白了就是跟别的线程分享点资源的进程,每个线程都有个独一无二的task_struct结构体,内核就是按这个来调度的。 这个task_struct就像进程的身份证,里边记录了它占了哪些内存(mm)、用了哪些文件系统资源(fs)、开了哪些文件(files)、收到了什么信号(signal),还有个PID。内核一般会通过slab分配器大批量生产这些task_struct结构,新建的结构就在栈底,后面跟着struct thread_info信息,串成一条链子。PID的总数要看pid_max的值,默认是32768,你可以用cat /proc/sys/kernel/pid_max看一下。系统为了快速找PID,用了哈希表加链表和树这种组合方式来管理。 进程的一辈子大概是这样的:从就绪状态开始,可能跑着跑着就进了运行态或者睡眠态、停止态,最后变成僵死态。深度睡眠那种必须得等资源来了才能醒的状态最让人头疼;而浅睡眠只要资源或者信号一来就能把它叫醒。停止态通常是人为的暂停,用Ctrl+Z就能让程序停下来干活,不过这玩意儿醒了以后不一定马上就能干活,得先排到就绪队列里去。 要想造个新进程,不管你用标准的fork还是clone,最后都得去调用do_fork这个幕后黑手。这个函数干的活就是新建一个task_struct结构,不同的参数会让结果不一样。比如你要是设置了独立的地址空间,那就是一个新的进程;要是想跟父进程共用地址空间,那就是个用户线程;要是只用内核栈共享,那就是内核线程。 这里有个大家伙叫僵尸进程,就是那个任务跑完了资源也都释放了,但父进程还没来得及用wait系列函数把它的PID收走。这时候你杀它杀不死,它就是个短暂的幽灵。你用ps aux | grep defunct就能看到状态栏上有个Z字母。 以前有人为了给CPU限速用了个cpulimit工具,做法是把目标进程关进停止态再周期性地唤醒它来模拟低负载。比如你想把PID是12296的那个程序的CPU使用率压到10%以下,命令就是cpulimit -l 10 -p 12296。这种办法虽然简单粗暴但精度不太高。 作业控制有两种思路:一种是程序自己因为资源不够主动睡了;另一种是你用Ctrl+Z人为暂停它。两者有本质区别:前者是被动挨饿没办法;后者是主动执行的命令。 内核里有个叫wait queue的队列机制用来管进程睡觉的事儿。多个进程可以把自己挂在同一个队列上排队等着资源来叫醒自己。像两次Page fault发生的时候,系统会把进程踹进深度睡眠去读硬盘数据再回来继续跑。 咱们来写个简单的代码练练手: ```c include include include int main() { while (1) { fork(); printf("hello"); wait(NULL); // 不回收子进程会堆积大量僵尸 } } ``` 这段代码看起无限循环生孩子(fork),其实因为有wait()在那等着回收资源,所以不会出现僵尸爆炸的情况。要是把wait()给删了,输出栏立马就被一堆defunct僵尸给淹没了。 最后咱们说说内存泄漏的事儿:“进程死了却占着内存不还”这种说法不完全对。真正的泄漏是指进程还活着但内存一直在长。你可以连续多点拍几个样本来观察RSS值是不是随时间直线上升;如果增长停了或者下降了,基本就能排除是泄漏了。