文章目录
- 前言
- 一、初识fork
- 1.1 fork函数的介绍
- 1.2 fork出的子进程存在形式
- 1.3 写时拷贝
- 二、进程的状态
- 2.1 Linux内核源代码
- 2.2 理解内核链表(重要)
- 2.3 运行状态
- 2.4 阻塞状态
- 2.5 挂起状态
- 三、Z(zombie)状态 ,僵尸进程
- 四、 孤儿进程
- 总结
前言
本文将介绍如何利用系统调用来在创建进程,并进一步了解进程的状态,以及相对应延伸出来的僵尸进程和孤儿进程等知识点
一、初识fork
Linux中的fork函数是进程创建的核心机制之一。它允许从一个已经存在的进程中创建一个新的进程(子进程),并通过写时拷贝技术优化内存使用。
1.1 fork函数的介绍
我们在函数内部使用fork时它会创建一个子进程,并且它会返回两个pid_t类型值(实际上就是整形),子进程的pid会返回给父进程,0会返回给子进程,创建错误返回-1。
而返回两个值的具体的实现方法主要依赖于操作系统的进程管理和上下文切换机制(当前不做过多赘述,只需要知道有这个东西就好了)。并且由task_struct链表形式可知每个进程都是具有独立性的!
当他返回两个值的时候我们就可以根据if语句来对父子进程做出不同的代码逻辑实现了。
1.2 fork出的子进程存在形式
父进程与子进程的存在是 1 :n 存在的,这意味着一个父进程可以有多个子进程,但一个子进程只有一个父进程。这也是父进程内部代码使用fork函数创建子进程的时候返回子进程id并返回0给子进程的原因。
并且子进程的PCB跟父进程的PCB相同。这意味着两者的exe和cwd相同,意味着指向同一块代码和数据。
1.3 写时拷贝
写时拷贝(Copy On Write,简称COW)是一种优化技术,主要用在计算机编程中,特别是在处理字符串或大型数据结构时
定义:
写时拷贝是指在多个对象或变量共享同一块内存空间时,如果其中一个对象或变量试图修改这块内存中的数据,系统就会为修改者分配一块新的内存空间,并将原始数据复制到这块新空间中,然后让修改者在新空间中操作数据,而其他对象或变量仍然共享原始的内存空间。
二、进程的状态
由于状态也是进程的一个属性, 所以进程的状态就是task_struct内的一个整数!
2.1 Linux内核源代码
- 为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状态(在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):意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
• D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个
状态的进程通常会等待IO的结束。
• T停⽌状态(stopped):可以通过发送SIGSTOP信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送SIGCONT信号让进程继续运⾏。
• X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
2.2 理解内核链表(重要)
在前面的认知中,我们说进程的数据结构是双链表,但是它同时也可以存在于队列结构中,这是为什么呢?
我们平时设计的双链表结构是这样子的
但是在PCB及task_struct中并不是这么设计的,它没有定义同类型指针next和prev!!它选择在自己的成员变量中增加一个结构体,这个结构体只定义了同类型指针next和prev,然后task_struct将这个结构体包含。
细节上是:原本的设计会指向整个结构体,但是增加一个结构体成员指针变量后,就通过这个list_head,而指向另外task_struct结构体的list_head而不会指向整个task_struct
我们在c语言阶段知道有个东西叫做偏移量,当我们想访问整个task_struct结构体的时候,就可以通过lit_head的偏移量(&(struct task_struct*)0->links)计算出task_struct结构体地址并通过强转成task_struct,代码为(struct task_struct*)(next - (struct task_struct*)0->links),这意味着lsit_head结点可以存储task_struct的位置信息
以下总结很重要!!:
就相当于list_head这个结构体存储了task_struct的位置信息,如果想要树结构我们再定义一个struct tree_node放入task_struct里面就可以构成树结构了!!
一个task_struct可以包含右很多的类似list_head结点,因此一个task_struct能被放到双链表或者队列来进行操作。一个PCB在内核中只存在一份,但它可以隶属于多种数据结构,意味着我们可以把这个task_struct根据不同的需求来变成双链表或者队列等含节点的数据结构进行操作。
由此引出以下概念:
进程状态的变化,表现之一就是在不同的队列中进行流动,本质都是数据结构的增删改查
2.3 运行状态
Runqueue(运行队列)是Linux内核中用于管理和调度进程的核心数据结构之一。Runqueue是Linux内核调度程序中的数据结构,用于存储给定处理器(CPU)上的可执行进程。
当一个进程被放到运行队列里面的时候,就是运行状态
2.4 阻塞状态
当进程由于等待某个事件(如I/O操作完成、资源可用、信号量等)而暂时无法继续执行。此时这个状态就叫做进程的阻塞状态
此时细节上的发生就是身处调度队列中的一个PCB因为执行到某个代码块(例如scanf函数需要我们输入时),这个PCB会流动到另一个队列中,等到事件结束
以下是一个简单的示例,用于说明进程从运行状态到阻塞状态的流动过程:
- 假设有一个进程正在执行一个需要从键盘读取数据的操作(如使用scanf函数)。
- 当进程执行到scanf函数时,它会等待用户输入数据。此时,由于用户尚未输入数据,进程无法继续执行,因此会进入阻塞状态。
- 操作系统会将该进程的PCB从运行队列中移出,并放入到键盘的等待队列中。
- 当用户输入数据并按下回车键时,键盘中断被触发,操作系统会读取输入数据并将其放入到内核输入缓冲区中。
- 随后,操作系统会通知该进程数据已经就绪,进程会从键盘的等待队列中移出,并重新进入运行队列等待被调度执行
2.5 挂起状态
- 进程的执行被暂时停止,并且其数据被保存在内存中,但不会被调度执行。
- 挂起可以由系统主动发起(如内存不足时,将不活跃进程挂起),也可以由用户请求(如调试、暂停进程)。
- 挂起状态可以是“就绪挂起”(Ready Suspended)或“阻塞挂起”(Blocked Suspended),分别对应于就绪和阻-塞状态的挂起。
三、Z(zombie)状态 ,僵尸进程
当一个进程退出时会释放其占用的系统资源,但是其PCB并不会消失,task_struct里面会记录一些自己完成退出的信息,返回给父进程
- 僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后
⾯讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程- 僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
- 所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态
总结:僵尸进程就是子进程结束后,父进程没有处理子进程的返回状态,并且这个过程很可能造成内存泄漏!
四、 孤儿进程
产生原因如下
- 子进程是通过父进程创建的。子进程的结束和父进程的运行是一个异步过程,即父进程无法准确预测子进程何时会结束。当一个父进程由于正常完成工作而退出,或者由于其他情况(如异常终止)被操作系统终止时,如果它的一个或多个子进程仍在运行,那么这些子进程就会失去父进程,成为孤儿进程。
特点如下:
- 无父进程控制:孤儿进程失去了原父进程的直接控制,但它们仍然能够继续运行并完成剩余的工作。
- 被init进程收养:操作系统会自动将孤儿进程的父进程设置为init进程 (在Unix/Linux系统中,init进程通常是PID为1的进程,而在现代系统中可能是systemd),由init进程负责后续对孤儿进程的管理和回收。(由前面僵尸进程中的知识点可知子进程一定是要靠父进程回收处理的,但如果一个父进程先结束了,那么子进程就会被1号进程托管)
- 无害性:与僵尸进程不同,孤儿进程并不会对系统造成危害。因为init进程会确保它们在完成工作后得到妥善处理。
总结:孤儿进程是操作系统中一种常见的进程状态,其产生原因和处理机制都是操作系统设计中的重要考虑因素。通过init进程的收养和回收机制,可以确保孤儿进程在完成工作后得到妥善处理,从而避免系统资源的浪费。
总结
本文主要讲解了进程的创建初步认识,并说明了为什么一个task_struct可以做不同的数据结构去操作,然后把进程的重要状态描述一遍。