文章目录
- 创建两个进程
-
- [**2. 实现思路及源代码**](#2. 实现思路及源代码)
-
- [2.1 实现思路](#2.1 实现思路)
-
- [2.1.1 `fork()` 函数](#2.1.1
fork()
函数) - [2.1.2 思路分析](#2.1.2 思路分析)
- [2.1.1 `fork()` 函数](#2.1.1
- [2.2 源代码](#2.2 源代码)
-
- [2.2.1 源代码分析](#2.2.1 源代码分析)
- [2.2.2 源代码测试结果](#2.2.2 源代码测试结果)
- [**3. 打印进程树**](#3. 打印进程树)
-
- [3.1 tmux操作步骤](#3.1 tmux操作步骤)
-
- [3.1.1 启动 `tmux`](#3.1.1 启动
tmux
) - [3.1.2 分屏操作(Ctrl+b是在告诉系统准备输入一个快捷键)](#3.1.2 分屏操作(Ctrl+b是在告诉系统准备输入一个快捷键))
- [3.1.3 切换窗口操作](#3.1.3 切换窗口操作)
- [3.1.4 在新分屏中运行 `pstree`](#3.1.4 在新分屏中运行
pstree
) - [3.1.5 退出 `tmux`](#3.1.5 退出
tmux
)
- [3.1.1 启动 `tmux`](#3.1.1 启动
- [3.2 打印进程树结果](#3.2 打印进程树结果)
- [**4. 源码分析**](#4. 源码分析)
-
- [4.1 Linux0.12中涉及的主要源码](#4.1 Linux0.12中涉及的主要源码)
- [4.2 结合源码分析实验代码](#4.2 结合源码分析实验代码)
-
- [4.2.1 进程创建 (`fork`)](#4.2.1 进程创建 (
fork
)) - [4.2.2 具体进程状态转换](#4.2.2 具体进程状态转换)
- [4.2.1 进程创建 (`fork`)](#4.2.1 进程创建 (
- [**5. 实验过程中遇到的问题及解决方法**](#5. 实验过程中遇到的问题及解决方法)
-
- [问题5.1 fork_two_children.c在运行过程中过快结束,来不及打印](#问题5.1 fork_two_children.c在运行过程中过快结束,来不及打印)
- [问题5.2 单窗口在休眠状态下无法打印进程树](#问题5.2 单窗口在休眠状态下无法打印进程树)
- **6.参考链接**
-
- 进程概念------PCB详讲
- 【Linux系列】进程PCB控制管理详解
- [[【linux系统】进程(进程PCB + 进程地址空间+进程控制)](https://blog.csdn.net/ProcedureStone/article/details/142786072?ops_request_misc=\&request_id=\&biz_id=102\&utm_term=进程PCB\&utm_medium=distribute.pc_search_result.none-task-blog-2\~all\~sobaiduweb\~default-1-142786072.142\^v102\^control\&spm=1018.2226.3001.4187)](#【linux系统】进程(进程PCB + 进程地址空间+进程控制))
创建两个进程
2. 实现思路及源代码
2.1 实现思路
2.1.1 fork()
函数
fork()
是 Linux/Unix 系统中的一个系统调用,用于创建一个新的进程。这个新的进程(称为子进程)是父进程的复制品,并从 fork()
语句之后开始执行。
fork()
返回值如下:
- 在父进程中:返回子进程的 PID。
- 在子进程中:返回 0。
- 失败时:返回 -1。
2.1.2 思路分析
由2.1.1可知,在处于子进程时,fork返回的PID=0;在处于父进程时,fork返回的PID>0;而实验需要创建两个子进程,也就是在父进程完成第一个子进程创建后,通过if判断语句,判断当前是否处于父进程(PID>0);如果处于父进程,则再次创建子进程。这样就
举个例子:假设父进程 PID 是 1000
,执行的顺序如下:
- 父进程
1000
调用fork()
,创建 子进程 1 (1001
)。 - 子进程 1 (
1001
) 进入if (pid1 == 0)
分支 ,打印信息后return
退出。 - **父进程
1000
继续执行pid2 = fork();
,创建 子进程 2 (1002
)。 - 子进程 2 (
1002
) 进入if (pid2 == 0)
分支 ,打印信息后return
退出。 - 父进程
1000
继续执行,打印父进程对应的信息。
2.2 源代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid1, pid2;
pid1 = fork(); // 创建第一个子进程
if (pid1 < 0) {
perror("fork failed");
return 1;
}
if (pid1 == 0) {
// 子进程1
printf("Child 1: PID = %d, Parent PID = %d\n", getpid(), getppid());
sleep(15); // 保持进程存活
return 0;
} else {
pid2 = fork(); // 创建第二个子进程
if (pid2 < 0) {
perror("fork failed");
return 1;
}
if (pid2 == 0) {
// 子进程2
printf("Child 2: PID = %d, Parent PID = %d\n", getpid(), getppid());
sleep(15); // 保持进程存活
return 0;
} else {
// 父进程
printf("Parent: PID = %d, Child1 PID = %d, Child2 PID = %d\n", getpid(), pid1, pid2);
sleep(15); // 父进程也保持存活
}
}
return 0;
}
2.2.1 源代码分析
pid1 = fork(); // 创建第一个子进程
-
第一个
fork()
调用后:如果返回 0 (子进程):子进程执行
if (pid1 == 0)
内的代码。如果返回非 0(父进程):父进程继续执行后续代码。
-
此时,进程数量已经变成 2 个 (一个是原始父进程,另一个是
pid1
产生的子进程)。if (pid1 == 0) {
// 子进程1
printf("Child 1: PID = %d, Parent PID = %d\n", getpid(), getppid());
sleep(15);
return 0;
} -
第一个子进程会直接 return,不会继续往下执行
fork()
相关的代码。pid2 = fork(); // 创建第二个子进程
-
第二个
fork()
调用后:(前提条件是pid1>0,处于父进程)
如果返回 0 (子进程):子进程执行 if (pid2 == 0)
内的代码。
如果返回非 0(父进程):父进程继续执行后续代码。
此时,此时,进程数量从 2 个变成 3 个:
- 原始的父进程
- 第一个子进程(
pid1
) - 第二个子进程(
pid2
)
2.2.2 源代码测试结果
s:~/os_exp/exp2_process$ ./fork_two_children
Parent: PID = 1585110, Child1 PID = 1585111, Child2 PID = 1585112
Child 1: PID = 1585111, Parent PID = 1585110
Child 2: PID = 1585112, Parent PID = 1585110
3. 打印进程树
3.1 tmux操作步骤
3.1.1 启动 tmux
tmux
3.1.2 分屏操作(Ctrl+b是在告诉系统准备输入一个快捷键)
-
垂直分屏(左右分屏):
Ctrl + b,然后按 %
-
水平分屏(上下分屏):
Ctrl + b,然后按 "
3.1.3 切换窗口操作
-
在不同的分屏间切换:
Ctrl + b,然后按 方向键(←/→/↑/↓)
3.1.4 在新分屏中运行 pstree
-
在其中一个分屏中运行:
./fork_two_children
-
切换到另一个分屏,运行:
pstree -p 父进程端口号
3.1.5 退出 tmux
-
关闭当前窗口:
exit
3.2 打印进程树结果
~/os_exp/exp2_process$ pstree -p 1585110
fork_two_childr(1585110)─┬─fork_two_childr(1585111)
└─fork_two_childr(1585112)
4. 源码分析
4.1 Linux0.12中涉及的主要源码
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,pgrp,session,leader;
int groups[NGROUPS];
/*
* pointers to parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->p_pptr->pid)
*/
struct task_struct *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
unsigned long timeout,alarm;
long utime,stime,cutime,cstime,start_time;
struct rlimit rlim[RLIM_NLIMITS];
unsigned int flags; /* per process flags, defined below */
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
struct m_inode * library;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
};
PCB通常包含的内容 | |||
---|---|---|---|
进程描述信息 | 进程控制和管理信息 | 资源分配清单 | 处理机相关信息 |
进程标识符(PID) | 进程当前状态 | 代码段指针 | 通用寄存器值 |
用户标识符(UID) | 进程优先级 | 数据段指针 | 地址寄存器值 |
-
在多道程序系统中,多个进程都要在CPU上运行,有时还要申请使用其他资源,由于资源的宝贵性,使得并非每个进程都能立即得到资源,从而导致进程之间的竞争(竞争是由两个以上进程以显式或隐式的方式共享资源而引起的状态)。
-
一般情况下进程有三种状态,就绪(资源,CPU),运行(资源,CPU),阻塞(资源,CPU)。
-
Linux在每个进程的task_struct结构中,定义了state域来描述进程的调度状态,共有五种,定义如下:
#define TASK_RUNNING 0 // 进程正在运行或已准备就绪。
#define TASK_INTERRUPTIBLE 1 // 进程处于可中断等待状态。
#define TASK_UNINTERRUPTIBLE 2 // 进程处于不可中断等待状态,主要用于I/O操作等待。
#define TASK_ZOMBIE 3 // 进程处于僵死状态,已经停止运行,但父进程还没发信号。
#define TASK_STOPPED 4 // 进程已停止。
4.2 结合源码分析实验代码
4.2.1 进程创建 (fork
)
在实验代码中,调用fork()
两次,创建了两个子进程。在Linux 0.12中,fork()
的执行流程如下:
-
系统调用入口 :
fork()
系统调用会触发sys_fork()
函数(在kernel/system_call.s
中定义),进而调用copy_process()
(在kernel/fork.c
中)。 -
复制进程 :
copy_process()
会为新进程分配PCB,即task_struct
结构,并从父进程的PCB复制大部分信息(如CPU寄存器、文件描述符等),且会为新进程分配唯一的pid
(进程号),将子进程的state
设置为TASK_RUNNING
(就绪态),表示进程可以被调度执行,同时把新创建的进程加入调度队列,等待CPU调度。
4.2.2 具体进程状态转换
(1)初始状态
- 父进程处于
TASK_RUNNING
状态,执行fork()
创建Child 1
进程。
(2)第一次fork()
Child 1
被创建,其PCB信息从父进程复制,状态为TASK_RUNNING
。Child 1
的pid
被分配,ppid
指向父进程。- 由于
fork()
的返回值不同:- 父进程继续执行,并进入下一步创建
Child 2
。 Child 1
进入自己的分支,执行printf
然后调用sleep(15)
,使其进入TASK_INTERRUPTIBLE
(睡眠状态)。
- 父进程继续执行,并进入下一步创建
(3)第二次fork()
Child 2
被创建,task_struct
再复制一次。Child 2
的pid
被分配,ppid
仍指向父进程。- 父进程继续执行,
Child 2
进入自己的分支执行printf
然后sleep(15)
,进入TASK_INTERRUPTIBLE
状态。
(4)运行状态
- 进程调度器会根据
counter
字段和优先级选择进程运行。 - **如果时间片耗尽或进程主动
sleep(15)
,状态转换为TASK_INTERRUPTIBLE**
,等待时间到达或信号唤醒。
(5)结束状态
Child 1
和Child 2
完成sleep(15)
后,调用return 0;
,退出进程。- 在Linux 0.12中,进程退出时:
- 进入
TASK_ZOMBIE
状态,等待父进程调用wait()
回收资源。 - 如果父进程没有及时
wait()
,子进程的task_struct
仍然保留在进程表中(僵尸进程)。
- 进入
- 当父进程执行完
sleep(15)
后,也会结束,最终所有进程都退出,系统回收资源。
5. 实验过程中遇到的问题及解决方法
问题5.1 fork_two_children.c在运行过程中过快结束,来不及打印
解决方法:调用sleep()
函数让进程进入休眠(sleep)状态,即在指定的秒数内暂停执行,从而支持用户进行进程树的打印操作
问题5.2 单窗口在休眠状态下无法打印进程树
解决方法:学习了tmux 操作,通过打开水平窗口,并进行窗口的切换操作,从而通过 pstree -p 端口号 进行进程树的打印操作,详细操作方案可参考3.1内容