当执行一个可执行程序时,会先把可执行程序加载到内存中。将程序加载到内存中并执行的时候,操作系统为该程序创建一个进程,进程中包含程序的代码、数据、PCB(用于描述和管理进程状态的数据结构),进程地址空间(描述进程在内存中布局的数据结构),页表(管理进程内存的重要数据结构)等。
所以进程 = 内核数据结构+程序
操作系统创建进程时,会创建一个PCB(进程控制块)的数据结构,操作系统从此管理进程,不会去管理进程的程序,将其转换对特定数据结构的管理,先将进程状态描述起来,在将其用特定的数据结构组起来(Linux下用的是双向链表)。
进程的信息可以通过 /proc 统文件夹查看
在Linux系统中,/proc
文件夹提供了对内核和正在运行的进程的信息的访问。
ps
列出当前用户进程的简要信息
非root用户
root用户
ps aux
:显示所有进程信息这,包括用户、PID(进程ID)、CPU利用率、内存利用率等。
top命令
:以交互式的方式显示系统中运行的进程信息。它实时更新
htop命令
:它以直观和友好的方式显示系统的实时性能信息。相比于经典的 top
命令,htop
提供了更多的功能和更直观的用户界面。top的完美替代品
task_strut是操作系统内部数据,想要访问,必须通过系统调用。
在Linux中普通进程都有父进程,在命令行中启动的进程都是bash(命令行解释器)的子进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("PID = %d\n",getpid());
printf("PPID = %d\n",getppid());
return 0;
}
这段代码就可以查看当前进程的pid和ppid
当前进程的ppid就是bash
fork:在Linux系统中,fork
是一个用于创建新进程的系统调用。fork
调用会创建一个与父进程几乎完全相同的子进程,这两个进程在执行时拥有相同的代码、数据。
可以使用man 2 fork
简单认识一下fork系统调用
fork
调用会返回两次:一次在父进程中,一次在子进程中。fork
返回子进程的进程ID(PID);在子进程中,fork
返回0。fork
返回-1。#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("Heloo proc :pid = %d,ppid = %d,ret = %d\n",getpid(),getppid(),ret);
sleep(1);
return 0;
}
这个程序执行结果:
可以看出,这个程序会执行两次打印。
子进程的代码和数据从哪里来?
fork返回值:
注意:上面代码中一个变量ret会保存两个不同的值,这和进程地址空间有关。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0 ) return -1;
else if(id == 0)
{
//子进程
while(1)
{
printf("i am child ,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父进程
while(1)
{
printf("i am parent ,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
让子进程循环打印自己的信息,让父进程打印自己的进程,一个程序两个死循环一直执行
运行结果:
操作系统创建进程时把进程的状态信息保存在进程PCB中,进程的状态主要有运行、阻塞、挂起状态
一般每个CPU核心拥有一个运行队列,当进程PCB放到CPU的运行队列中,准备好随时被CPU调度时,就称之为运行状态
int main()
{
int a = 0;
scanf("%d",&a);
printf("%d\n",a);
return 0;
}
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 *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
所谓的状态本质就是一个变量,不同的值表示不同的状态。
wait()
来获取其终止状态。#include<stdio.h>
int main()
{
while(1);
return 0;
}
运行上面的程序,使用ps命令查看进程信息
STAT列就是进程状态信息列,上面进程的状态是R+,也就是运行状态。
后台进程:
运期间还可以运行其他程序,并且不能用ctrl+c 命令杀死
kill
命令用于向进程发送信号。默认情况下,kill
发送的是 SIGTERM
(终止)信号,通知进程正常退出。
kill-l
:可以查看kill命令的所有信号
kill <PID>
默认使用的是15号信号IGTERMkill -9 <PID>
强制终止进程pkill <process_name>
根据程序名终止进程wait
或waitpid
等系统调用),就会造成僵尸进程。孤儿进程
。#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//孤儿进程demo
pid_t id = fork();
if(id < 0 )
{
perror("fork");
return -1;
}
else if(id == 0)
{
//子进程
printf("i am child process pid = %d, ppid = %d\n",getpid(),getppid());
sleep(3);//子进程等待三秒后退出
printf("child process exit\n");
}
else{
//父进程
printf("partent process ,pid = %d\n",getpid());
exit(0);//父进程直接退。
}
return 0;
上面程序运行结果如下
父进程先退出,子进程被1号进程领养。三秒后,子进程执行结束.
用ps -l
命令查看系统进程
PRI:
NI:
在 top
命令中可以交互式地修改进程的优先级。top
提供了一个交互式的界面,你可以在其中选择要调整优先级的进程,并对其进行一些操作,包括修改 Nice 值。
当前进程的PRI是默认值80
使用top命令修改优先级
1:输入top命令
2:按入r在输入要修改进程的pid
3:输入nice值([-20,19]之间)
修改之后的优先级
在Linux系统下进行验证:
#include <stdio.h>
#include <stdlib.h>
int g_unval;
int g_val = 0;
int main(int argc, char* argv[],char* envp[])
{
const char* str= "Hello World";
printf("code_address:%p\n",str);//代码段
printf("g_val_address:%p\n",&g_val);//初始化数据段(全局变量)
printf("g_unval_address:%p\n",&g_unval);//未初始化数据段
int*p = (int*) malloc(sizeof(int));
printf("heap_address:%p\n",p);//堆区
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/606f90be65a44626a275913aa6fb39ff~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1544&h=788&s=150650&e=png&b=000006)
printf("stack_address:%p\n",&str);//栈区
for(int i = 0 ;argv[i]; ++i)
{
printf("argv_address:%p\n",argv[i]);//命令行参数
}
for (int i = 0; envp[i]; i++)
{
printf("envp_address:%p\n",envp[i]);//环境变量
}
return 0;
}
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
}
else if(id == 0)
{
//child
g_val = 100;//子进程中修改全局变量g_val
printf("i am child process, g_val=%d,&g_val= %p\n",g_val,&g_val);
}
else
{
sleep(2);
printf("i am parent process,g_val=%d,&g_val=%p\n",g_val,&g_val);
}
return 0;
}
让子进程先执行,修改全局变量的值,再让父进程执行,
运行结果:
发现和预想的不一样,本应该子进程修改后,全局变量的值已经变为了100,而父进程打印时还是0,并没有被修改,而且同一块地址空间,存了两个不同的值。
这可以验证,在语言层面上,所有的地址都是虚拟地址,如果是真实的物理内存,就不会出现一块地址空间存放两个不同的值。物理地址用户是看不到的,由操作系统进行统一管理。
在上面的例子中,父子进行中的虚拟地址是相同的,但是变量的值是不同的。
这里由操作系统将虚拟地址映射到物理内存中,从而实现,语言层面上的一块地址存放不同的值。
进程的地址空间信息被封装在 mm_struct
结构中。将其进行区域划分,
进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度。
在mm_struct结构体中,每一个字节都代表着一个虚拟地址,这些虚拟地址通过和页表与物理内存建立映射关系。
每个进程被创建时,对应的PCB,mm_struct,页表,都会随之被创建。
而操作系统可以通过进程的PCB找到其mm_struct。(PCB当中有一个结构体指针存储的是mm_struct的地址)
在用fork创建子进程时,操作系统以父进程为模板为子进程创建PCB,mm_strcut,页表等。
子进程的PCB,mm_stuct,页表和最初的父进程一样,(子进程和父进程的数据和代码是共享的),即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。这样可以节省内存和提高效率。
在上面的例子中,子进程修改全局变量g_val为100,操作系统会把父进程中的g_val在物理内存中拷贝一份,在进行修改。通过页表与子进程建立映射关系,从而实现了进程之间的独立性。