进程控制
主要需要通过进程控制回答以下问题:
在OS的支持下,进程具体是如何启动起来的?
详细介绍
程序是如何运行起来,演变为进程的
- 也就是通过
exec加载程序后,进程启动起来的具体过程
学完本部分就会知道:
双击快捷图标,或者在命令行执行./a.out后,程序运行起来的背后原理
有OS支持时,main函数return,或者使用exit、_exit所返回的返回值到底给了谁,有什么意义
为什么在平时,这个返回值我们会感觉到并没有什么用处
程序在内存中运行起来后就演变为了进程
- 但是由于java需要
虚拟机来解释执行,中间夹一个虚拟机,所以要单独理解java进程是如何产生的与java类似的C#、pyhton等的程序,都是相同的运行过程
什么情况下的程序才会涉及到多进程呢
不过一般我们写程序都是
单进程的
进程关系
基于OS运行的进程很多,众多进程之间往往存在着一定的关系,在这一部分会具体介绍进程之间有哪些关系
守护进程
OS启动起来以后,会有很多我们平时看不见的,但是一直在
默默运行的后台进程,这些后台进程大多其实是守护进程
- 对于程序员来说,编写程序时有各种语言的区分
- 但是
文字编码形式的程序一旦被转为机器指令被CPU执行时,对于CPU来说全都是一样的,没有任何区别
进程时,都有着相同的启动过程进程控制的API,可以用来加载运行所有语言编写的程序将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程
| 存储位置 | 存在状态 | 运行过程 | |
|---|---|---|---|
| 程序 | 硬盘 | 静态 | 无 |
| 进程 | 内存上,从硬盘拷贝的副本 | 动态 | 进程有生有死 |
有OS支持时,会有很多的进程在运行,这些进程都是
并发运行的
并发运行:
就是CPU轮换的执行
当前进程执行了一个短暂的
时间片(ms)后,切换执行另一个进程,如此循环往复
- 由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的
- 但是在微观上cpu每次只执行某一个进程的指令
当然我们这里说的单核cpu的情况
多核CPU:并发与并行是同时存在的
OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID
PID用来唯一标识一个进程
如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的
PID放在了该进程的task_struct结构体变量中
进程在运行的过程中:OS会去管理进程,这就涉及到很多的管理信息;
OS为了管理进程,会为每一个进程创建一个task_struct结构体变量,里面放了各种的该进程的管理信息
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的一部分
#include
#include
//获取调用该函数进程的进程ID(哪个函数调用它,返回的就是哪个)
pid_t getpid(void);
//p:parent p:process
//获取调用该函数进程的父进程ID
pid_t getppid(void);
#include
#include
//获取调用该函数进程的用户ID(在哪个用户下调用该进程)
uid_t getuid(void);
//获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID
uid_t getgid(void);
返回值:返回各种ID值,不会调用失败,永远都是成功的
#include
#include
#include
#include
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
程序运行的三个过程:
(1)在内存中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
(3)pc指向第一条指令,cpu取指运行
当有OS时,以上过程肯定都是通过调用相应的API来实现的。
在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec
fork:开辟出一块内存空间
exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了
运行起来的进程会与其它的进程切换着并发运行
#include
#include
//pid_t 无符号整型
//从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程
pid_t fork(void);
复制后有两个结果:
父进程内存空间样子,原样复制地开辟出子进程的内存空间代码和数据和父进程完全相同复制父进程的主要目的:
就是为了复制出一块内存空间
只不过复制的附带效果是,子进程原样的拷贝了一份父进程的代码和数据
事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码
由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数
"当然这个说法有些欠妥,但是暂且这么理解"
1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
2)子进程的fork,成功返回0,失败返回-1,errno被设置。
#include#include #include #include 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 */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
#include
#include
#include
#include
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条件各自做不同的事

父进程在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
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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;
}
#include
#include
#include
#include
#include
#include
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)父进程设置的锁,子进程不能被继承