什么是进程
根据冯诺依曼体系来说,任何程序都需要先加载到内存当中,然后cpu从内存中去取,然后进行解析,执行指令。当一个程序被加载到内存的时候系统就已经为其分配了一块空间了,并且会将程序的指令,数据和其他资源加载到这段内存当中,这个时候系统已经为它创建了进程了。当cpu开始解析、执行指令的时候就叫做进程开始运行了。
在Windows下,可以通过任务管理器查看进程
在Linux下可以通过以下指令来查看进程
ps axj
top
但我们一般查看进程的时候是执行以下语句
ps axj | head -1 && ps axj| grep 12929
12929部分为要查询的进程内容
ps axj表示查看所有的进程,通过管道,执行head -1,表示只拿第一行的内容
&&是连接符,和C/C++当中一样,&&前面执行成功之后才会执行&&后面的内容
打印了第一行的结果之后,再将进程的所有内容通过管道传过去执行grep 12929,表示打印进程中关于12929的所有的内容
自己创建一个进程
#include<stdio.h>
int main(void)
{
while(1)
{
printf("我是一个进程\n");
sleep(1);
}
return 0;
}
通过命令查询进程
ps axj | head -1 && ps axj | grep test
这里还有一个grep --color==auto test
是什么东西呢?
这个进程是由bash
创建的,因为执行grep这个指令的时候也需要创建一个进程,这样才会被cpu解析,执行。
描述进程PCB
在变成进程之前,操作系统需要对这个进程进行描述,它会创建一个结构体去存储一些信息,这个结构体叫做进程控制块,我们称之为PCB(process control block),在Linux操作系统下的PCB叫做**
task_struct
**。
PCB当中会存一些指针指向内存中的代码、数据以便可以找到这块的代码、数据。
什么是进程:进程就是内核关于进的相关数据结构+当前进程的代码和数据。
task_struct
task_struct内容分类:
- **标识符:**描述当前进程的唯一标识符,用于区分其他进程
- **状态:**任务状态、退出代码、退出信号等
- **优先级:**相对于其他进程的优先级
- **程序计数器:**程序中即将被执行的下一条指令的地址
- **内存指针:**包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- **上下文数据:**进程执行时处理器的寄存器中的数据
- **I/O状态:**包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
- **记账信息:**可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
Linux中是以双向链表把task_struct之间连接起来的。
获取进程的PID和PPID
PID是当前进程的id
PPID是当前进程的父进程的id
可以通过:getpid()、getppid()获取
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
通过fork创建进程
fork是一个系统调用级别的函数,它的功能就是创建一个子进程。
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("aaaaaa\n");
fork();
printf("bbbbbb\n");
return 0;
}
可以发现,明明只有一个printf("bbbbbb\n");
为什么会打印两次呢?
这是因为fork创建了一个子进程,两个进程自然就会打印两次。
接下来看一下创建的进程PID
和PPID
是多少。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc pid:%d ppid:%d, ret: %d\n", getpid(),getppid(), ret);
sleep(1);
return 0;
}
再看以下代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("begin:我是一个进程,pid:%d, ppid:%d\n",getpid(), getppid());
pid_t id = fork();
if(id > 0)
{
while(1)
{
printf("我是父进程,pid:%d,ppid:%d &id:%p\n",getpid(),getppid(),&id);
sleep(1);
}
}
else if(id == 0)
{
while(1)
{
printf("我是子进程,pid:%d,ppid:%d &id:%p\n",getpid(),getppid(),&id);
sleep(1);
}
}
else
{
perror("子进程创建失败!\n");
}
return 0;
}
这里为什么会一个进入子进程一个进入父进程的判断呢?变量id
可以同时存在两种值吗?
为什么fork给子进程返回0,给父进程返回pid?
一般来说,子进程PCB当中会存储父进程的PID
,但是父类的PCB当中不会存储子类的PID
,因此子类可以找到父类,但是父类找不到子类,因此子类返回0,父类返回子类的PID
。
fork是如何做到返回两次的?
当进程在运行的时候,都是具有独立性的,父子进程也是一样,在程序运行的时候,代码都是处于只读状态,当有一个执行流尝试修改数据的时候,当前进程就会触发写时拷贝
当创建一个子进程的时候,子类的PCB会以父类的PCB为模板创建出一个全新的PCB,但是子类是没有自己的代码和数据的,需要共享父类的数据和代码,但当数据发生改变的时候,就会发生写时拷贝。
fork是通过不同的返回值去调用不同进程的代码块。
一个变量怎么会有两个值呢?
因为进程之间是独立的,父子进程之间也是如此,不能因为子进程崩溃了父进程也运行不了了,而独立的进程之间肯定是需要有自己独立的代码,比如父进程当中有一个变量a=10,那么子进程当中也应该要有一个变量a=10,不过这两个变量肯定不是同一个,是相互独立的两个变量,但我们需要让子进程把父进程的所有的代码全都拷贝过来吗?显然是不用的,因为我们大部分情况下只会对其中一些变量进行修改,那么就没必要把所有的全都拷贝下来,因此Linux操作系统会在当变量发生改变的时候,重新开辟一块空间,把数据存进去,然后让其修改和管控这一片空间的数据。
Tips:共享代码并不影响独立性,因为代码在加载到内存之后是不可能发生改变的。
父子进程谁先运行
在调用fork函数创建子进程后,操作系统会根据调度算法来确定父子进程的运行顺序。一般情况下,父进程和子进程的执行顺序是不确定的,取决于操作系统的调度策略和当前系统的负载情况。
在某些操作系统中,例如Linux,通常会采用抢占式调度(preemptive scheduling),也就是说,操作系统会根据一定的优先级和时间片轮转的方式来决定进程的运行顺序。在这种情况下,父进程和子进程可能会交替运行,每个进程都会被分配一定的时间片来执行。
还有一种情况是,父进程可以通过调用wait函数等待子进程的结束,这样父进程会先运行,直到子进程结束后再继续执行。
总之,父进程和子进程的运行顺序是由操作系统的调度策略和当前系统的负载情况决定的,无法确定谁先运行。