目录
1️⃣ 进程的概念(Process)
1.什么是进程?
❍ 课本概念:程序的一个执行实例,正在执行的程序等
❍ 内核观点:担当分配系统资源(CPU时间,内存的实体)
简单来说:进程 == PCB(进程控制块) + 进程对应的代码和数据;一个进程对应一个PCB
操作系统对进程的管理,最终变成了对链表的增删查改。
注意:可执行程序加载到内存不是进程,只是进程对应的代码和数据
2.多进程管理
操作系统中可以存在大量的进程,操作系统需要管理这些进程,以确保系统资源(如CPU时间,内存等)的合理分配。管理进程的本质是对进程数据的管理
3. 描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的结合,课本上称之为 PCB(process control block),Linux操作系统下的PCB是:task_struct
task_struct 的结构
struct task_struct {
volatile long state;
void *stack;
atomic_t usage;
unsigned int flags;
unsigned int ptrace;
unsigned long ptrace_message;
siginfo_t *last_siginfo;
int lock_depth;
// ... 其他属性
};
task_struct
包含进程的所有属性数据,如进程状态、优先级、程序计数器、内存指针、上下文数据、I/O状态信息和记账信息等。
2️⃣ 进程的查看与管理
1.使用指令查看进程
ps指令:
语法:ps[选项]
功能:显示当前终端会话中属于当前用户的进程列表。
选项:
常见格式:ps -ajx | head -1 && ps -ajx | grep 可执行文件
2.通过系统调用函数查看pid
操作系统对进程进行管理,但是用户不能直接访问操作系统,因此需要通过系统提供的系统调用函数来管理进程。
✸ 查看pid的函数为getpid();
✸ 可以通过man手册查询,输入指令:man getpid
通过创建一个C语言代码来查看pid :
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){ pid_t id = getpid();
pid_t parent = getppid();
while(1)
{
printf("I am a process, pid: %d, ppid: %d\n", id, parent);
sleep(1);
}
return 0;
}
3.杀掉进程
方法1: crtl + c
;方法2: kill -9 pid
注意:kill掉任意一个进程不会影响另一个进程!!!(保证进程的独立性)
4.ppid(父进程id)
刚刚在演示的过程中,我已经把ppid的方法加入里面了,这里我就不多介绍使用了
我们直接看刚刚到代码
为什么进程每次启动pid会变,但是ppid不会变呢???
我们先查看一下父进程是什么,输入该命令: ps -axj | head -1 && ps -axj | grep ppid
通过查看可以看到该进程是bash进程(命令行解释器),因此就很好理解了。
❍ 当我们运行一个进程时,命令行解释器会把这个指令解释成bash的子进程。
❍ 接着再由这个bash的子进程执行对应的命令。
❍ 即:每一条命令被执行,都属于bash的子进程,只是子进程不一样。
为了更好看到执行程序与进程信息,可以使用shell脚本,隔一秒查一次进程
while : ; do ps ajx | head -1 && ps ajx | grep myprocess; sleep 1; done
3️⃣ 创建进程
在类Unix操作系统中,进程的创建通常是通过fork()
系统调用实现的。fork()
调用会创建一个与父进程几乎完全相同的新进程,这个新进程称为子进程。
fork()的工作原理
当fork()
被调用时,以下步骤大致发生:
◉ 内核数据结构:操作系统在内核中为子进程创建新的进程描述符(process descriptor),这是一个数据结构,包含了进程的所有信息,如进程ID(PID)、父进程ID(PPID)、状态、资源使用情况等。
◉ 复制地址空间:子进程会获得父进程地址空间的一个副本,这包括代码段、数据段、堆栈等。这个复制是写时复制(copy-on-write)的,意味着只有在任一进程尝试写入这些数据时,物理内存才会实际被复制。在fork()
调用后,子进程和父进程的地址空间是完全相同的。
◉ 执行流程:fork()
调用之后,子进程和父进程都从fork()
调用点之后继续执行。在父进程中,fork()
返回子进程的PID;在子进程中,fork()
返回0;如果fork()
调用失败,则在父进程中返回-1。
父进程与子进程的关系
◉ 代码和数据继承:子进程默认情况下继承了父进程的代码和数据。这是因为在fork()
调用时,并没有实际复制这些数据,而是采用了写时复制的技术。
◉ 独立性:尽管子进程继承了父进程的资源,但它是一个独立的进程,拥有自己的PID和PPID。子进程可以执行与父进程不同的代码路径,也可以通过exec()
系列函数加载并执行全新的程序。
◉ 资源分配:子进程会获得父进程打开的文件描述符、信号处理设置等资源的副本,但它们之间不会共享这些资源的状态(例如,文件偏移量是独立的)。
写时复制(Copy-on-Write)
写时复制是一种有效的资源管理技术,它可以减少不必要的内存复制操作。在fork()
之后,父子进程共享相同的物理内存页面,直到任一进程尝试写入这些页面。这时,操作系统会为写入者分配新的页面,从而保持两个进程的独立性。
fork()的使用
示例1️⃣
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
fork();//创建进程
printf("hello world,pid: %d,ppid: %d\n",getpid(),getppid());//查看进程对应信息
}
示例2️⃣
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("process is running,only me!,pid: %d\n", getpid());
sleep(3);
pid_t id = fork();
if (id == -1) return -1; //进程创建错误直接退出
else if (id == 0)
{
//child 子进程代码
while (1)
{
printf("id: %d,I am child process,pid: %d,ppid: %d\n", id, getpid(), getppid());
sleep(1);
}
}
else
{
//parent 父进程代码
while (1)
{
printf("id: %d,I am parent,pid: %d,ppid: %d\n", id, getpid(), getppid());
sleep(2);
}
}
return 0;
}
结果:
示例3️⃣ 一次创建多个进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
void RunChild()
{
while (1)
{
printf("I am a child process,pid: %d,ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
int main()
{
const int num = 5;
for (int i = 0; i < num; i++)
{
pid_t id = fork();
if (id == 0)
{
RunChild();//运行子进程代码
}
sleep(1);
}
while (1)
{
sleep(1);
printf("I am parent,pid: %d,ppid: %d\n", getpid(), getppid());
}
return 0;
}
结果:
[wuxu@Nanyi lesson12]$ vim myprocess.c
[wuxu@Nanyi lesson12]$ make
gcc -o myprocess myprocess.c -std=c99
[wuxu@Nanyi lesson12]$ ./myprocess
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am parent,pid: 18489,ppid: 17725
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am parent,pid: 18489,ppid: 17725
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am parent,pid: 18489,ppid: 17725
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am parent,pid: 18489,ppid: 17725
I am a child process,pid: 18495,ppid: 18489
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18490,ppid: 18489
I am a child process,pid: 18491,ppid: 18489
I am a child process,pid: 18492,ppid: 18489
I am parent,pid: 18489,ppid: 17725
I am a child process,pid: 18493,ppid: 18489
I am a child process,pid: 18495,ppid: 18489
^C
补充:
为了更好看到执行程序与进程信息,可以使用shell脚本,隔一秒查一次进程,且不查看grep进程信息。
while : ; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done
为什么要创建子进程
创建子进程是操作系统和多任务环境中常见的做法,它服务于多种目的和场景。以下是一些主要原因:
1.并行和并发执行
❍ 并行处理:通过创建子进程,可以在多个处理器核心上并行执行任务,从而提高程序的执行速度。
❍ 并发执行:在单个处理器上,子进程可以让多个任务看似同时进行,即并发执行。
2.资源隔离
❍ 隔离环境:子进程可以运行在独立的地址空间中,这样即使子进程崩溃,也不会影响父进程或其他子进程。
❍ 权限限制:子进程可以以不同的用户权限运行,从而限制对系统资源的访问。
3.任务分解
❍ 负载分配:一个大任务可以被分解成多个子任务,每个子任务由一个子进程执行,这样可以更有效地利用系统资源。
❍ 模块化设计:程序可以通过创建子进程来分离不同的功能模块,使得程序结构更清晰,易于维护。
4.进程控制
❍ 父子关系:子进程可以由父进程创建、监控和终止,这为进程控制提供了方便。
❍ 信号传递:父进程可以发送信号给子进程,以便于通信和同步。
5.程序设计
❍ 执行外部程序:使用fork()
和exec()
系统调用,一个程序可以启动另一个程序作为子进程执行。
❍ 程序扩展性:通过创建子进程,程序可以动态地根据需要增加或减少并行执行的实例。
6.实用场景
❍ 服务器模型:在许多服务器应用程序中,通常会为每个客户端连接创建一个子进程,以便并行处理多个请求。
❍ 后台任务:某些任务可以放在后台执行,不干扰前台的用户交互,例如日志处理、数据备份等。
❍ 管道和过滤器:在Unix命令行中,通过管道将一个命令的输出作为另一个命令的输入,通常涉及到子进程的创建。
7.错误处理和恢复
❍ 异常隔离:如果一个子进程遇到错误或异常,可以单独终止该子进程,而不影响其他进程。
❍ 容错机制:通过监控子进程的状态,可以实现故障检测和恢复机制。
创建子进程是操作系统中实现多任务、并行处理和资源管理的关键机制之一。然而,这也带来了一些挑战,如进程同步、通信和资源竞争等,需要在设计程序时予以考虑。
4️⃣ task_struct内容分类
❍ 标示符: 描述本进程的唯一标示符,用来区别其他进程。
❍ 状态: 任务状态,退出代码,退出信号等。
❍ 优先级: 相对于其他进程的优先级。
❍ 程序计数器: 程序中即将被执行的下一条指令的地址。
❍ 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
❍ 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
❍ I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
❍ 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
❍ 其他信息
5️⃣ 查看进程内容
◉ ls /proc/pid -d
# 按照目录查看
◉ ls /proc/pid -l
# 查看进程内容
测试内容
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while (1)
{
printf("I am process,pid: %d,ppid: %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
1.如果我们在此处把可执行程序给删除,进程还会不会运行呢???
此处删除程序并不会造成影响,因为程序是加载到内存的,也就是内存拷贝了一份,这里是不会造成影响的,因为所占内存小,但如果内存过大运行就会出现问题
2.当前工作路径有什么用呢?
我们在C语言中学习文件操作,fopen("log.txt","w");默认是在当前目录创建文件,但是我们不一定每次都在当前目录创建文件,那怎么才能在其他目录下创建文件呢?
修改文件创建的路径,我们需要一个命令:chdir
#include<stdio.h>
#include<unistd.h>
int main()
{
chdir("/home/wuxu");//更改工作目录为/home/wuxu
FILE* pf = fopen("log.txt", "w");//创建文件
(void)pf;// ignore system warning
fclose(pf);
while (1)
{
printf("I am a process,pid: %d\n", getpid());
sleep(1);
}
return 0;
}
因此我们对文件的理解不应该是在C语言上,而是应该在操作系统上