进程是什么?
用户角度:正在运行的程序
操作系统角度:操作系统对运行中程序的描述信息,即进程描述符,统称PCB
在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
PCB中的描述信息:
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 程序计数器:程序中即将被执行的下一条指令的地址
- 上下文数据:进程执行时处理器的寄存器中的数据
- 标识符PID(进程ID):描述进程的唯一标示符,用来区别其他进程
- 进程状态:任务状态,退出代码,退出信号等
- 进程优先级:相对于其他进程的优先级
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
- 文件的IO状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表
进程查看:ps -ef 或 ps -aux(查看详细的进程信息)
获取调用进程ID:
进程创建:
通过复制调用进程,创建一个新的进程(子进程)
创建子进程的意义:分摊任务处理压力;让子进程完成其它任务
复制:复制父进程的PCB(大部分数据,不是全部),即内存指针、上下文数据、程序计数器
父进程和子进程:代码共享,数据独有
返回值:判断是否创建成功 + 分辨父子进程
父进程:返回值是子进程的pid,pid是大于0的;创建子进程失败返回-1
子进程:返回值是0
用户可以通过fork的返回值不同对父子进程运行流程进行分流
在Linux系统中,除了系统启动之后的第一个进程由系统来创建,其余的进程都必须由已存在的进程来创建,新创建的进程叫做子进程,而创建子进程的进程叫做父进程。那个在系统启动及完成初始化之后,Linux自动创建的进程叫做根进程。根进程是Linux中所有进程的祖宗,其余进程都是根进程的子孙。具有同一个父进程的进程叫做兄弟进程。
#include <stdio.h>
#include <unistd.h>
int g_val = 100;
int main()
{
printf("hello world!---pid:%d\n", getpid());
//pid_t fork(void);
//创建一个子进程,父进程返回子进程的pid;子进程返回0
pid_t pid = fork();
if (pid < 0)
{
printf("fork error!\n");
return -1;
}
if (pid == 0)
{
//子进程与父进程代码共享、数据独有;即g_val的值是不同的
//子进程地址与父进程地址相同,但其实是虚拟地址相同
g_val = 200;
printf("------child------%d g_val:%d---%p\n", getpid(), g_val, &g_val);
}else {
sleep(2);
printf("------parent------%d g_val:%d---%p\n", getpid(), g_val, &g_val);
}
printf("nihao~~world!---pid:%d\n", getpid());
while(1)
{
printf("DreamCatching\n");
sleep(2);
}
return 0;
}
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址 。
Linux下进程状态
- 运行态(R):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
- 可中断休眠态(S):意味着进程在等待事件完成
- 不可中断休眠态(D):在这个状态的进程通常会等待IO的结束
- 停止态(T):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
- 僵死态(Z):这个状态只是一个返回状态,在任务列表里不会看到这个状态
ctrl+z:程序暂停,进入停止态
僵尸进程:处于僵死状态的进程
僵尸进程的产生原因:子进程先于父进程退出,退出原因保存在PCB中,操作系统检测到子进程退出后,通知父进程,但是父进程可能并没有关注子进程的退出(可能正在做其它事情),这时候操作系统不能随意释放子进程资源(因为父进程会关心子进程的退出原因),因此子进程就处于退出但是资源没有完全释放的状态(僵死状态)
危害:资源泄露,僵尸进程过多导致新进程无法创建
解决:退出父进程(退出原因的保存已经毫无意义)
如何避免产生僵尸进程:进程等待
kill:杀死进程
- kill -9(强杀,但是杀不死僵尸进程)
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,被init进程(1号进程)收养
守护(精灵)进程:特殊的孤儿进程
Linux下环境变量
环境变量:一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 查看环境变量:env
- 查看环境变量和普通变量:set
- 查看指定环境变量内容:echo
- 设置/声明一个环境变量:export
- 删除一个环境变量:unset
- 常见环境变量:HOME、PWD、SHELL、PATH
- 环境变量特性:全局特性(继承)
shell终端下所运行的进程能够获取到所有的环境变量,但是获取不到普通变量 - 环境变量在代码中的获取:
char* getenv(char* name);
main函数的第三个参数:int main(int argc, char* argv[], char* env[]);
声明全局变量:extern char** environ; - 环境变量的使用场景:通常是父进程通过给子进程设置环境变量来达到向子进程传递数据的功能
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[], char *env[])
{
//char *getenv(const char *name);
//通过环境变量名称获取内容
char *ptr = getenv("PWD");
printf("ptr:%s\n", ptr);
int i;
for (i = 0; env[i] != NULL; i++)
{
printf("env[%d] = [%s]\n", i , env[i]);
}
return 0;
}
程序地址空间(虚拟地址空间)
内存地址:内存区域的一个编号
虚拟地址—>虚拟地址空间
虚拟地址空间是用struct mm_struct结构体描述的
虚拟地址空间是什么?
我么们所看到的程序地址空间实际是一个虚拟地址空间,实际上是操作系统通过mm_struct结构体为进程描述的一个空间,也称为内存描述符。
为什么要使用虚拟地址空间?
- 能够让进程使用连续的内存地址;保证进程独立性;进行内存访问控制;提高内存利用率
- 进程通过访问虚拟地址进而获取变量数据,最终还是要去访问物理内存,因为数据是存储在物理内存中的
- 在虚拟地址和物理地址之间通过页表进行地址映射,转换得到物理地址,进而访问物理内存区域
- 通过映射之后,物理地址可就不一定连续了,通过这种映射转换的方式实现数据的离散存储来提高内存利用率
- 页表中不但记录了虚拟地址和物理地址的映射关系,并且还记录的这块地址的属性实现内存访问控制
写时拷贝技术:操作系统通过复制父进程创建子进程,子进程初始时与父进程指向同一块物理内存区域,当内存数据发生改变时,会为子进程重新开辟内存更新页表。
进程的独立性:进程应该具有独立性,因为独立才更加稳定。
页表是如何将虚拟地址转换为物理地址的?
分段式内存管理:
- 内存地址构成:段号+段内偏移
- 段表:有很多的段表项
- 通过地址中的段号去段表中找到段表项,通过段表项中的物理段起始地址+地址中的段内偏移获取到物理地址。
分页式内存管理:
- 内存地址构成:页号+页内偏移
- 页表:有很多页表项
- 通过地址中的页号去页表中找到页表项,通过页表项中的物理页号+页内偏移获取到物理地址。
段页式内存管理:
- 内存地址:段号+段内页号+页内偏移
- 段表项中包含段内页表起始地址;段内页表项中又包含物理页号
- 通过地址中的段号去段表中找到段表项,通过段表项中的段内页表地址找到段内页表,通过地址中的段内页号在段内页表中找到页表项,通过页表项中的物理页号+页内偏移获取到物理地址