前面的学习我们知道了进程实际上是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中(PCB进程控制块中)。这个进程控制块包含了一个进程运行时所需要的的所有信息。
前面还学过了进程地址空间,进程地址空间是将实际的内存抽象成一个线性的数组,而联系这两个中间桥梁是 页表。但是现在我们进程要对内存中的某个值进行修改,但是内存上面除了进程的数据意外还有内核等其他数据,如果进程修改的数据恰恰是内核数据,那么就会导致系统出问题,所以修改内存这种重要的任务是不能交给进程自己去修改,而是应该有操作系统完全托管。这就引出了 用户模式 和 内核模式
cpu处理器通常是用某个控制寄存器中的一个模式位来提供这种功能,当设置了模式位时,进程就运行在内核模式,一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存
处在用户模式中,进程不允许执行特权命令(这里可以理解成修改一切与硬件和操作系统的指令),比如:停止处理器、改变模式位、或者发起一个I/O操作。也不允许用户模式的进程直接引用地址空间中内核区的代码和数据
进程休眠函数:
#include
unsigned int sleep(unsigned int seconds);
参数
unsigned int seconds
:进程要休眠的时间
返回值
剩余要休眠的秒数,如果时间已经达到预定的秒数则返回0
Linux中有一个重建进程非常重要的函数——fork()
,fork以该进程为模板,创建一个子进程,而原进程为父进程
#include
pid_t fork(void)
现在我们以进程的角度来看待一下进程的创建:
fork函数实际上是一个系统调用,调用fork函数之后,就会被内核接管接下来的流程,内核会做如下事情:
当一个子进程被创建出来:
一个程序 是由两部分组成:程序 = 代码 + 数据
由于父子进程执行的是同一份代码,且代码又是不可以修改的,所以父子进程共享代码,所以父子进程的虚拟地址代码段在内存中的映射应该是同一块空间
但是数据是绝对不能共享的! 这是依据进程的独立性!数据必须每个进程私有一份,那是什么是进程的独立性?打个比方:你关闭你电脑上的画图板,会不会QQ顺带也退出了?进程之间不会相互影响,靠的就是数据各自私有!
但是上图中好像父子进程数据也指向了同一块空间,这其实是Linux操作系统对内存做的特殊处理—— 写时拷贝
由于子进程从 PCB 到 虚拟地址空间 到 页表 都是完全拷贝的是父进程,父子进程的数据就是同一块空间了。如果子进程需要修改数据,那么只要修改相应数据所对应的页表的指向,指向一块新的空间即可,例如:父子进程共享一块10M的空间,如果子进程要修改2M空间的数据,那么只需要修改这2M空间的页表,使其指向内存上新开辟的一块2M的空间即可:
为什么要写时拷贝?
一个进程的数据可能会非常大,且不是所有数据子进程都立马使用,所以并不是所有数据都要拷贝一份给子进程,如果在创建进程的时候就直接拷贝所有数据的话,把本来可以在后面考别的,甚至不用拷贝的的数据都拷贝了,非常浪费时间。所以直接把数据和子进程共享,但是子进程对这部分数据是只读,如果要增删、修改,就要另外开辟空间了。
进程在运行结束的时候就会终止,但是一个进程在退出的时候有三种状态:
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
一个进程在退出的时候会用一个int返回他的运行的最终结果,而这个int就叫做进程的退出码。进程的退出码返回给父进程,让父进程拿到子进程的执行状况。
例如:我们在C/C++代码中在main函数结尾都要写一个return 0
,0就是main的退出码。那为什么写成0呢?
那是因为一般在函数设计中:0代表执行正确,非零例如:1、2、3、4…对应的是一种错误原因。
我们可以用$?
在shell命令行中查看最近一次进程的退出码
下面介绍一下进程退出的方法:
return+退出码
这种退出方式是终止函数,返回的是函数的退出码。但如果是main函数的return那就是进程的退出码
#include
void exit(int status);
其中exit就是进程的退出码,注意和return区别,return是函数 的退出码,exit是进程的退出码!
#include
void _exit(int status);
_exit 和 exit的功能和用法都是完全相同的,但是区别在于:
下面的两段代码就可以证明:
#include
#include
#include
int main()
{
printf("hello world");
exit(1);
}
#include
#include
#include
int main()
{
printf("hello world");
_exit(1);
}
进程等待是进程控制中最重要的一个概念。首先我们要知道为什么要进程等待?
子进程被创建出来之后,父子谁先运行,是由调度器说了算,而谁先结束由于父子进程执行的代码可能不同,所以更加不确定。所以这时候就需要进程等待,让父进程等子进程退出之后再退出
为什么父进程要在子进程之后退出?
进程等待时的状态
父进程:进程被阻塞,本质是操作系统不调度,进程状态变成S状态
子进程:进程执行结束后, 释放数据代码,但是PCB不被释放,进入僵尸状态,等待父进程回收
#include
#include
pid_t wait(int *status);
参数:status是一个输出型参数
返回值:成功返回被等待进程的pid,失败返回-1
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
参数:
0
或WNOHANG
WNOHANG
代表若pid指定的子进程没有结束,则waitpid()
返回0,不予以等待。若正常结束,则返回该子进程的pid 。这种方式叫做非阻塞等待 返回值
WNOHANG
,而调用中waitpid发现没有已经退出的子进程可收集,则返回0获取子进程的status:
status只有前16位有效,然而这16位分为高八位,和低八位
如果一个进程是正常退出的话,那么他的低八位就是全0,而高八位是他的退出码。退出码可以用status >> 8
如果一个进程是被信号杀掉(也就是上面进程终止所说的异常退出),那么他的第八位就不为0,而终止信号是前七位可以用status & 0x7f
来得到。
阻塞等待 和 非阻塞等待
阻塞等待:等待的进程会一直挂起直到被等待的进程执行结束
非阻塞等待:等待的进程不会挂起,只是返回被等待进程的状态。非阻塞等待更加偏向于监视,可以通过对此非阻塞等待来达到阻塞等待的效果!
举个简单的例子:阻塞等待类似于一个进程放下手上的所有事情全程检查子进程的状态,而非阻塞等待类似于抽查,进程对子进程的状态进行一次检查,并通过返回值返回进程是否 等待 成功
如何理解阻塞等待?
本质是将父进程从运行队列拿到等待队列进行等待(将父进程状态设置为非R状态),直到子进程执行结束,再把父进程从等待队列拿到运行队列继续运行
阻塞等待代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 int ret=fork();
9
10 if(ret==0)
11 {
12 int count=5;
13 while(count--)
14 {
15 printf("i am son!\n");
16 sleep(1);
17 }
W> 18 int a=1/0;
19 exit(0);
20 }
21 int status=0;
22 waitpid(ret,&status,0);
23 printf("i am father\n");
24 if((status & 0x7f) == 0 ) //进程正常退出 下面判断退出码来判断结果是否正确!
25 {
26 printf("进程正常退出!\n");
27 printf("退出码是:%d\n",(status>>8) & 0xff);
28 }
29 else // 进程异常退出
30 {
31 printf("进程异常退出!\n");
32 printf("中断信号是:%d\n",status & 0x7f);
33 }
34 return 0;
35 }
非阻塞等待代码
#include
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 int status=0;
9 int pid=fork();
10 if(pid==0)
11 {
12 int count=5;
13 while(count--)
14 {
15 printf("i am son!\n");
16 sleep(1);
17 }
W> 18 int a=1/0; //抛出异常
19 exit(0);
20 }
21 else
22 {
23 int ret=0;
do{
25 ret = waitpid(-1,&status,WNOHANG);
26 if(ret==0)
27 {
28 printf("child is running!\n");
29 sleep(1);
30 }
31 }
32 while(waitpid(ret,&status,WNOHANG)==0);
33
34 if((status & 0x7f) == 0 ) //进程正常退出 下面判断退出码来判断结果是否正确!
35 {
36 printf("进程正常退出!\n");
37 printf("退出码是:%d\n",(status>>8) & 0xff);
38 }
39 else // 进程异常退出
40 {
41 printf("进程异常退出!\n");
42 printf("中断信号是:%d\n",status & 0x7f);
43 }
44
45 }
46 return 0;
47 }
创建子进程的目的:
父进程创建子进程之后,父子进程就会执行共享的代码,数据私有一份。但是进程替换是将子进程的代码数据被完全替换,从而达到执行另外一个程序的目的,但是被替换后的子进程pid未被改变(同时也说明没有创建新的进程)
#include
int execl(const char *path, const char *arg, ...); //...是可变参数列表:命令行怎么执行传入什么选项,你就可以在这里直接按照顺序写入参数
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
这些函数都是由exec函数演化出来的,后面所加的v、p、l、e实际上代表了一些特殊含义:
v(vector)
:参数用数组的形式传进去p(path)
:有p自动搜索环境变量PATH,例如上面没有带p的第一个参数就要写出完整路径,而带p的只需要写出文件名即可。但是指令不在PATH包含的路径中的时候就要把路径写全!l(list)
:参数用列表的形式传入e(env)
:表示自己维护的环境变量总结:
返回值
exec系列函数正常情况下是没有返回值的,如果替换出现错误,那么就会返回-1
代码调用示例:
l
的 1 #include<unistd.h>
2 #include<stdio.h>
3
4 int main()
5 {
6 printf("进程替换!\n");
7 execl("/usr/bin/ls","ls","-a","-l",NULL); //如果exec后面带有p那么第一个参数可以不用写成绝对路径,直接写成ls即可
8 }
v
的1 #include<unistd.h>
2 #include<stdio.h>
3
4 int main()
5 {
6 printf("进程替换!\n");
7 char * const p[]={"ls","-a","-l",NULL}; //用指针数组代替
8 execv("/usr/bin/ls",p);
9 }
e
的文件1:test.c
#include
#include
#include
int main()
{
char * const p[]={"./a.out",NULL};
char * const env[]={"myname=tony",NULL};
execve("./a.out",p,env);
exit(0);
return 0;
}
文件2:test1.c
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 printf("环境变量myname的值为%s\n",getenv("myname"));
7 return 0;
8 }
然后再shell上执行如下命令:
gcc test.c -o execve.exe
gcc test1.c
./execve.exe
注意