Linux操作系统及进程(二)子进程的创建及进程状态

本文详细解析了Linux中的fork操作,包括其原理、返回值,以及进程的不同状态(运行、阻塞、挂起、僵尸状态),讨论了僵尸进程的危害,如内存泄漏和孤儿进程问题。
摘要由CSDN通过智能技术生成

目录

一、fork创建子进程

1.1运行 man fork 认识fork

1.2fork原理及返回值

二、进程状态

2.1排队

2.2状态

 2.3运行、阻塞、挂起

三、Linux进程状态

状态演示

僵尸进程危害


一、fork创建子进程

1.1运行 man fork 认识fork

fork有两个返回值 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。

cb65321b02b24505a09a420b18973871.png

1cca496e708e471f8e40f242d359f8a6.png

可以看到程序执行了两边after fork,这是因为在fork处创建了一个子进程,子进程有和父进程一样的代码从fork开始,子进程开始执行而父进程还会继续往下执行,所以子进程执行了一遍printf,父进程也执行了一遍printf。

19517就是父进程的pid而19518就是子进程的pid,而15094则是bash

9a5c7c4f82d443fb8c5599ba6639ca51.png

查看fork可以得知

5c6096eada234e2abf8991c3fe691b29.png

fork成功后将子进程pid返回给父进程返回0给子进程,如果失败就返回-1.

75909738cfa14af9b1008ecca8234a2b.png e22747fa2f0a440f8a35e09c8a137543.png

通常我们可以通过下面的代码操作来对父子进程进行分流操作,让父子进程去执行不同的代码,实现不同的功能。

1446a80da19e4846af9cc764da5e8407.png

1.2fork原理及返回值

fork在创建子进程后,将子进程的pid返回给父进程,将0返回给子进程,这是因为父子进程之间是1:n的关系,一个父进程可以创建多个子进程,而每个子进程只能对应一个父进程,而pid标识符具有唯一性,所以父进程想要找到具体的子进程就必须要有子进程的pid。

而进程之间都具有独立性,不能相互影响,OS在设计时就必须保证这一点,所以父子进程之间也不能相互影响,子进程继承父进程的代码和属性,当子进程尝试对代码进行修改操作时就会发生写时拷贝。

二、进程状态

既然有了父子进程,它们各自执行,那进程在执行时都处于什么样的状态呢?

2.1排队

6ad62df835714127858f6aa11d59d5e4.png

拿以上代码举例,当程序执行到scanf时,就会停下等待输入。

所以进程并不是一直在运行的,它可能在等待某种硬件资源。

即使进程放在了CPU上,也不是一直会运行的,每个进程都有一个时间片,所以在日常写代码过程中,即使我们写了死循环也不会把操作系统搞挂。

进程=内核数据结构(task_struct)+可执行程序。而所谓的进程的排队,并不是进程本身的可执行程序在排队,而是task_struct即PCB在排队。

而一个task_struct可以被链入多种数据结构中,而在Linux内核中,它不是被链入到一个单链表当中的而是被链入到一个双链表中的,如下图所示

1d85bb203de044f89a332f6805763b50.png

每个task_struct中都有一个类似于struct_listnode的结构体,里面存放的就是这个PCB的上一个地址和下一个地址。同样的指针指向的也不是其他task_struct本身而是其他struct_listnode的地址。可是怎么通过这些地址来拿到task_struct中的其他数据呢?

小到整形大到结构体,在拿到一个地址要对整个数据访问时,都会涉及到一个名为偏移量的东西,拿上图举例,操作系统在进行存储时,是从低到高进行空间的使用的,先使用低地址,再使用高地址,所以&n=&t+偏移量,现在我们知道&n想要拿到&t,那么&t=&n-偏移量就可以得到初始地址。

偏移量=(task_struct*)0->n;

起始地址=(task_struct*) (&n-&((task_struct*)0->n));

而一个task_struct中也不止存在一个这样的struct_listnode。此时就可以做到,每一个PCB即可以被链入到全局双链表也可以被链入用户想链入的任意一个队列中。所以进程的增删就转变成对链表的增删。

2.2状态

83423c51150943f69ce01d375b8b65b8.png

1、 所谓的状态,本质就是一个整形变量,在task_struct中的一个整形变量。

9078e80c279c4db6b3c07dfb028dfc66.png

1868e181293c484a98d29222a63e85dc.png

所谓的状态就是task_struct中status的值对应上面的1,2,3。

2、状态决定了该进程的后续动作

Linux中可能存在多个进程都要根据它的状态执行后续动作。

一个CPU维护一个运行队列

6f5d6e839a0b4284a041222f223b36b5.png

 2.3运行、阻塞、挂起

操作系统管理对应的硬件都是先描述再组织。

所以类似于scanf之类的进行执行时,该进程就会被链入硬件的链表,状态改为阻塞,并且将该进程PCB从运行队列中剥离下来投递到底层的等待队列中。

而硬件的就绪状态只有操作系统最清楚。当键盘完成输入操作后,操作系统就会再次把该进程链入到运行队列中,将其状态改为运行状态,继续让CPU去调度向后执行。

所以所谓的状态变更本质上就是把进程投递到不同的队列中。

854bb940350d429fb0d0c6f470f34414.png

 而挂起状态有个前提:计算机资源(内存)较为吃紧。

例如在阻塞状态下,把进程相关的代码数据交换写入到外设例如磁盘当中,而在磁盘中也存在一个swap分区专门用来在操作系统内存吃紧时和操作系统进行数据换入和换出的,从而释放对空间的部分压力。此时一旦进程所对应的数据不在内存当中,此时我们就将该进程称为挂起状态。(注意,PCB即task_struct是不会被换出的 )所以也可以得知,在创建一个进程时一定是先创建内核数据结构即PCB的。

三、Linux进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

 S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠也称为浅度睡眠)通常就是所谓的阻塞状态。

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,D状态也是一种阻塞状态。

T停止状态(stopped): kill-19可以通过发送 SIGSTOP 信号给进程来停止(T)进程,进程被停止后会由前台进程转变为后台进程。kill-18这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。同样在gdb调试时也可以让程序进入T状态此时是一种tracing stop状态。

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态 。

Z状态也叫僵尸状态:是指进程已经死亡(结束)代码和可执行程序已经被释放,但是PCB内核数据结构即当前状态要维持住,供上层读取,此时它的PCB在被父进程读取之前会被操作系统保存,此时就是僵尸状态,如果父进程不读取,那么这个僵尸状态将会一直存在,此时就会产生内存泄漏。

状态演示

睡眠和运行

dc770d765d4b4302bbc3bb8bce0b699b.png

STAT下面所表示状态后面的加号表示其是一个前台进程,如果在运行可执行文件时在后面带上&符号,操作系统就会在执行时将其变成后台程序,此时想要杀死进程Ctrl C已经不能做到了,此时只能使用kill-9强行杀掉进程。

039021b66149462a8f937aaeb8612b4d.png

 7075a1a037b24c72a732ea8abe4ac065.png

僵尸状态

d99950a11d564250a30ac504a2647df8.png

6caf5df750a4495c93c3df8ae1a1d1e6.png  

通过上图代码,我们让子进程运行5秒后提前结束,但是父进程继续运行,不去读取子进程信息,此时子进程就是Z状态。

那么如果此时修改代码,让父进程比子进程先结束会发生什么呢,这里代码就不进行演示了,直接看结果:

07f0ea8f042a47f08656654b50c08d75.png

 父进程提前结束后,子进程的ppid就变成了1,这个1就是操作系统,而Linux中对这个现象做出非常形象的解释,此时这个继承的父进程提前结束,而子进程被操作系统领养,此时这个子进程就被称为孤儿进程,而在它变成孤儿进程的同时,它也会变成后台进程,只能用kill-9杀掉.

僵尸进程危害

1.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态

2.维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护

3.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
4.内存泄漏

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C+五条

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值