学习重点
进程控制
主要需要通过进程控制回答以下问题:
在OS的支持下,进程具体是如何启动起来的?
详细介绍
程序是如何运行起来
,演变为进程的- 也就是通过
exec加载程序
后,进程启动起来的具体过程
学完本部分就会知道:
双击快捷图标,或者在命令行执行./a.out后,程序运行起来的背后原理
- 也就是通过
有OS支持时,main函数return,或者使用exit、_exit所返回的返回值到底给了谁,有什么意义
为什么在平时,这个返回值我们会感觉到并没有什么用处
- java的进程是如何运行起来的
程序在内存中运行起来后就演变为了进程
- 但是由于java需要
虚拟机来解释执行
,中间夹一个虚拟机,所以要单独理解java进程是如何产生的
与java类似的C#、pyhton等的程序,都是相同的运行过程
什么情况下的程序才会涉及到多进程呢
不过一般我们写程序都是
单进程
的
进程关系
基于OS运行的进程很多,众多进程之间往往存在着一定的关系,在这一部分会具体介绍进程之间有哪些关系
守护进程
OS启动起来以后,会有很多我们平时看不见的,但是一直在
默默运行的后台进程
,这些后台进程大多其实是守护进程
进程控制
- 对于程序员来说,编写程序时有各种语言的区分
- 但是
文字编码形式的程序
一旦被转为机器指令
被CPU执行时,对于CPU来说全都是一样的,没有任何区别
- 所有程序(c++,java)在Linux OS上运行起来,演变为
进程
时,都有着相同的启动过程 - 有关
进程控制的API
,可以用来加载运行
所有语言编写的程序
进程相关知识
将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程
存储位置 | 存在状态 | 运行过程 | |
---|---|---|---|
程序 | 硬盘 | 静态 | 无 |
进程 | 内存上,从硬盘拷贝的副本 | 动态 | 进程有生有死 |
多进程并发运行
有OS支持时,会有很多的进程在运行,这些进程都是
并发运行
的
并发运行:
就是CPU轮换的执行
当前进程执行了一个短暂的
时间片
(ms)后,切换执行另一个进程,如此循环往复
- 由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的
- 但是在微观上cpu每次只执行某一个进程的指令
当然我们这里说的单核cpu的情况
多核CPU:并发与并行是同时存在的
进程号
OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数
),这个编号就是PID
PID用来唯一标识一个进程
如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的
- 进程号在系统调用时可以作为传入参数或者返回值
PID放在了哪里?
PID放在了该进程的task_struct结构体变量中
进程在运行的过程中:OS会去管理进程,这就涉及到很多的管理信息;
OS为了管理进程,会为每一个进程创建一个task_struct结构体
变量,里面放了各种的该进程的管理信息
三个特殊进程0,1,2
0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行
这三个进程非常重要
进程 PID == 0
这个进程被称为
调度进程
功能是实现进程间的调度和切换
该进程根据调度算法,让CPU轮换的执行所有的进程
当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换,同时保存被切换进程的现场
这个进程怎么来的? 这个进程就是由OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0的进程。 由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。
进程 PID == 1
-
作用:
- 初始化
- 托管孤儿进程
- 原始父进程-----就是它是最父的进程,用来创建子进程
初始化
这个进程被称为
init进程
作用是:
它会去读取各种各样的系统文件,使用文件中的数据来初始化OS的启动,让我们的OS进入多用户状态,也就是让OS支持多用户的登录
- 这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序
- 该PID==1的程序代码放在了
/sbin/init
下,当OS启动起来后,OS会去执行init程序,将它的代码加载到内存,这个进程就运行起来了
-
进程 PID == 2
- 该进程是守护进程或精灵进程,精灵进程也叫守护进程
- 作用:专门负责虚拟内存的请页操作
当OS支持虚拟内存机制时,加载应用程序到内存时,并不会进行完整的代码拷贝,只会拷贝当前要运行的那部分代码,当这部分代码运行完毕后,会再拷贝另一部分需要运行的代码到内存中,拷贝时是按照一页一页来操作的,每一页大概4096字节,这就是
换页操作
。
与调度进程一样,也是一个系统进程,代码属于OS的一部分
获取进程相关的ID函数
#include <sys/types.h>
#include <unistd.h>
//获取调用该函数进程的进程ID(哪个函数调用它,返回的就是哪个)
pid_t getpid(void);
//p:parent p:process
//获取调用该函数进程的父进程ID
pid_t getppid(void);
#include <unistd.h>
#include <sys/types.h>
//获取调用该函数进程的用户ID(在哪个用户下调用该进程)
uid_t getuid(void);
//获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID
uid_t getgid(void);
返回值:返回各种ID值,不会调用失败,永远都是成功的
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(void){
//pid_t getpid(void);
pid_t pid=getpid();
printf("%u\n",pid);
//pid_t getppid(void);
pid_t pid_parent=getppid();
printf("%u\n",pid_parent);
//uid_t getuid(void);
uid_t uid=getuid();
printf("%d\n",uid);
//uid_t geteuid(void);
uid_t gid=getgid();
printf("%d\n",gid);
return 0;
}
父进程就是当前终端窗口bash,因为在这个窗口执行的。
vi /etc/passwd
guojiawei❌1000:1000:guojiawei,:/home/guojiawei:/bin/bash
fork()
程序运行的三个过程:
(1)在内存
中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝
到划出的内存空间中
(3)pc指向第一条指令,cpu取指
运行
当有OS时,以上过程肯定都是通过调用相应的API
来实现的。
在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec
fork:开辟出一块内存空间
exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了
运行起来的进程会与其它的进程切换着并发运行
#include <sys/types.h>
#include <unistd.h>
//pid_t 无符号整型
//从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程
pid_t fork(void);
复制后有两个结果:
- 依照
父进程内存空间
样子,原样复制地开辟出子进程的内存空间 - 由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的
代码和数据
和父进程完全相同
复制父进程的主要目的:
就是为了复制出一块内存空间
只不过复制的附带效果是,子进程原样的拷贝了一份父进程的代码和数据
事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码
由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数
"当然这个说法有些欠妥,但是暂且这么理解"
1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
2)子进程的fork,成功返回0,失败返回-1,errno被设置。
#include <sys/types.h> #include <unistd.h> #include <sys/types.h> #include <stdio.h> int main(void){ //pid_t fork(void); pid_t ret=fork(); printf("%d,ret=%d\n",getpid(),ret); while(1);//为了能够查到进程pid,不然程序结束后会被杀死 return 0; } /* guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 12648,ret=12649 12649,ret=0 */
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(void){
pid_t ret=fork();
/*区分父子进程*/
if(ret >0){//父进程返回值是子进程pid,大于0
printf("parent PID=%d\n",getpid());
printf("parent ret=%d\n",ret);
}
else if(ret ==0){//子进程返回0
printf("child PID=%d\n",getpid());
printf("child ret=%d\n",ret);
}
printf("hello,world\n");
return 0;
}
guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out
parent PID=13154
parent ret=13155
hello,world
child PID=13155
child ret=0
hello,world
上述代码:父子进程各有一份拷贝,但是父子进程可以根据if条件各自做不同的事
复制的原理
- Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的对应的还是物理内存。
- 复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据
父进程在os上基于物理内存复制出一系列数据结构得到虚拟内存;子进程fork时候,完全复制父进程的代码和数据,而且在新开辟的内存中存储和父进程一样的内容
此时只要通过exec在子进程中加载新的程序,就可以实现开辟内存的效果
父子进程各自执行哪些代码
int main(void){
printf("before fork()\n");
pid_t ret=fork();
/*区分父子进程*/
if(ret >0){//父进程返回值是子进程pid,大于0
printf("parent PID=%d\n",getpid());
printf("parent ret=%d\n",ret);
}
else if(ret ==0){//子进程返回0
printf("child PID=%d\n",getpid());
printf("child ret=%d\n",ret);
}
printf("after fork\n");
return 0;
}
guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out
before fork()
parent PID=14206
parent ret=14207
after fork
child PID=14207
child ret=0
after fork
父子进程代码都相同
- 父进程会执行fork()之前的语句,子进程只会执行fork()之后的语句
- 父子进程的返回值不同,会根据if条件选择性执行
尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行
解释子进程打印出:before fork()
- 首先子进程复制了整个父进程程序,但是子进程不会执行fork()之前的语句
- printf()是行缓冲,如果没\n,也就是没写满一行,会挤压在缓冲区
- 这里的before fork()是直接从父进程复制的数据结构,然后随着第一个\n一起打印
父子进程共享操作文件
分为两种情况:
情况1:父子进程分别独立打开同一个文件
情况2:fork之前打开文件,也就是父进程事先打开,父子进程共享
独立打开文件
涉及到的两个进程是父子关系,独立打开同一文件实现共享操作
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void){
pid_t ret=0;
int fd=0;
ret=fork();//创建子进程
if(ret>0){//父进程操作
fd=open("mm.txt",O_RDWR|O_CREAT,0664);
write(fd,"hello\n",6);
}
else if(ret==0){//子进程操作
fd=open("mm.txt",O_RDWR|O_CREAT,0664);
write(fd,"world\n",6);
}
return 0;
}
/*
执行结果:world-------子进程覆盖了父进程
*/
原因:
由于父子进程单独打开文件,所以父子进程各自的进程表中,存放着
单独的文件表
,笔尖位置相同,由于打开同一个文件,最终节点指针指向同一个V节点
,从而发生了覆盖
独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表
如果不想相互覆盖,需要加O_APPEND标志。
#include <sys/types.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(void){ pid_t ret=0; int fd=0; ret=fork();//创建子进程 if(ret>0){//父进程操作 fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664); write(fd,"hello\n",6); } else if(ret==0){//子进程操作 fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664); write(fd,"world\n",6); } return 0; }
fork之前打开文件
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void){
pid_t ret=0;
int fd=open("mm.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
ret=fork();//创建子进程
if(ret>0){//父进程操作
write(fd,"hello\n",6);
}
else if(ret==0){//子进程操作
write(fd,"world\n",6);
}
return 0;
}
子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件
像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”
0,1,2文件:
子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的,同样的父进程的0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的
init进程
会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,之后所有进程的0 1 2,实际上都是从最开始的init进程那里继承而来的。
子进程会继承父进程的属性
(1)用户ID,用户组ID
(2)进程组ID
(3)会话期ID
(4)控制终端
(5)当前工作目录
(6)根目录(/)
(7)文件创建方式屏蔽字(unmask)
(8)环境变量
(9)打开的文件描述符
等等
子进程
独立
的属性:
(1)进程ID
(2)不同的父进程ID
(3)父进程设置的锁,子进程不能被继承