赞
踩
什么是程序:编译好的二进制文件。
什么是进程:运行着的程序。
站在程序员的角度:运行一系列指令的过程。
站在操作系统角度:分配系统资源的基本单位。
两者区别:
1、程序占用磁盘,不占用系统资源。
2、内存占用系统资源(CPU和内存)。
3、一个程序对应多个进程。(一个电脑能运行多个QQ)
4、一个进程只能对应一个程序。(一个进程不能基于这个程序启动,也不能基于那个程序启动。)
5、程序没有生命周期,进程有生命周期。
MMU(内存管理单元):包括从逻辑地址到虚拟地址(线性地址)再到内存地址的变换过程、页式存储管理、段式存储管理、段页式存储管理、虚拟存储管理(请求分页、请求分段、请求段页)。
MMU位于CPU内部,可以假想为一个进程所需要的资源都放在虚拟地址空间里面,而CPU在取指令时,机器指令中的地址码部分为虚拟地址(线性地址),需要经过MMU转换成为内存地址,才能进行取指令。
MMU完成两大功能:1.虚拟地址到内存地址的地址变换;2.设置修改CPU对内存的访问级别。 比如在Linux的虚拟地址空间中,3-4G为内核空间,访问级别最高,可以访问整个内存;而0-3G的用户空间只能访问用户空间的内容。其实这也是由MMU的地址变换机制所决定的。对于Inter(英特尔)CPU架构,CPU对内存的访问设置了4个访问级别:0、1、2、3(如上图所示),0最高,4最低。而Linux下,只是使用了CPU的两种级别:0、3。CPU的状态属于程序状态字PSW的一位,系统模式(0),用户模式(1),CPU交替执行操作系统程序和用户程序。0级对应CPU的内核态(特权态、管态、系统态),而3级对应用户态(普通态或目态),这其实是对内核的一种保护机制。例如,在执行printf函数的时候,其本身是在用户空间执行,然后发生系统调用,调用系统函数write将用户空间的数据写入到内核空间,最后把内核的数据刷到(fsync)磁盘上,在这个过程中,CPU的状态发生了变化,从0级(用户态)到3级(内核态)。
转载链接: https://blog.csdn.net/qq_33883085/article/details/88730969
.
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体。
struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
(1)进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
(2)进程的状态,有就绪、运行、挂起、停止等状态。
(3)进程切换时需要保存和恢复的一些CPU寄存器的值。
(4)描述虚拟地址空间的信息(如虚拟地址与物理地址之间的映射关系)。
(5)描述控制终端的信息(桌面终端、文字终端和设备终端,pts/0或者tty0等)。
(6)当前工作目录(当前进程的工作目录)。
(7)umask掩码(对文件的一种保护机制,文件权限)。
(8)文件描述符表,包含很多指向file结构体的指针。
(9)和信号相关的信息。
(10)用户id和组id。
(11)会话(Session)和进程组(功能相似的一些进程组成一个进程组)。
(12)进程可以使用的资源上限(Resource Limit)。(ulimit –a命令可以查看)
PATH
可执行文件的搜索路径。
比如ls命令就是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的可执行程序,比如test时,却需要提供完整的路径名./test,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含test所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:echo $PATH
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
getenv 函数
功能:获取当前进程环境变量
参数说明:
name环境变量名
返回值:
成功:指向环境变量的指针
失败:返回NULL
fork:分叉
功能:创建一个新的进程。
返回值:
成功:两次返回,父进程返回子进程的id,子进程返回0。
失败:返回-1。
功能:获取进程id。
获得当前进程的id: pid_t getpid(void);
获得当前进程父进程的id: pid_t getppid(void);
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main(int argc,char **argv) 6 { 7 printf("Begin......\n"); 8 pid_t pid = fork(); 9 if(pid < 0) 10 { 11 perror("fork err"); 12 exit(1); 13 } 14 if(pid==0) 15 { 16 // 子进程 17 printf("I am a child,pid = %d,ppid = %d.\n",getpid(),getppid()); 18 } 19 else if(pid > 0) 20 { 21 // 父进程的逻辑 22 printf("child_pid = %d,self_pid = %d,ppid = %d.\n",pid,getpid(),getppid()); 23 } 24 printf("End......\n"); 25 26 return 0; 27 }
运行结果:
代码执行流程相当于:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc,char **argv) { printf("Begin......\n"); pid_t pid = fork(); if(pid < 0) { perror("fork err"); exit(1); } if(pid==0) { // 子进程 printf("I am a child,pid = %d,ppid = %d.\n",getpid(),getppid()); while(1) { printf("I am a child.\n"); // 让子进程活着 sleep(1); } } else if(pid > 0) { // 父进程的逻辑 printf("child_pid = %d,self_pid = %d,ppid = %d.\n",pid,getpid(),getppid()); while(1) { printf("I am a father.\n"); // 让父进程活着 sleep(1); } } printf("End......\n"); return 0;
ps是查看进程相关信息的指令。
主要参数aux,其中,a是显示更多的all,u是user用户信息。x是通常和a组合起来使用。
ps ajx是查看进程组的相关信息,可以追溯进程之间的血缘关系。
可以看到shell进程是我们所写进程的父进程。
杀进程的命令是:kill -9 pid号
杀父进程:
显示killed
再使用ps ajx看一下:
父进程变成了1,也就是最上面的那个pid为1进程init把它给领养了。
这里kill -9底是什么意思呢?
使用kill -l指令看一下。
-9 其实就是传递标号为9的信号SIGKILL。
所以也可以使用kill -SIGKILL pid号来杀死进程。
假设 n = 5
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc,char** argv) { int n = 5; int i=0; pid_t pid; for(i=0;i<5;i++) { pid = fork(); if(pid < 0) { perror("fork err"); exit(1); } else if(pid==0) { // son printf("I am child ,pid = %d, ppid = %d.\n",getpid(),getppid()); } else if(pid > 0) { // father printf("child_pid = %d,self_pid = %d,ppid = %d.\n",pid,getpid(),getppid()); } } while(1) { sleep(1); } return 0; }
原因是:
在循环的过程中不让子进程产生新的进程。
只需要在代码中添加break,退出循环体。
结果:
第1个子进程是i=0时创建的,
第2个子进程是i=1时创建的,
…
第5个子进程是i=4时创建的,
当i = 5时,父进程也退出循环体,所以父进程对应的i的值是5。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc,char** argv) { int n = 5; int i=0; pid_t pid; for(i=0;i<5;i++) { pid = fork(); if(pid < 0) { perror("fork err"); exit(1); } else if(pid==0) { // son printf("I am child ,pid = %d, ppid = %d.\n",getpid(),getppid()); break; //子进程退出循环的接口 } else if(pid > 0) { // father //printf("child_pid = %d,self_pid = %d,ppid = %d.\n",pid,getpid(),getppid()); } } sleep(i); if(i<5) { printf("I am child,will exit,pid = %d,ppid = %d.\n",getpid(),getppid()); } else { printf("I am parent,will out,self_pid = %d,ppid = %d.\n",getpid(),getppid()); } return 0; }
结果:
刚fork之后,父子进程之间有哪些相同、哪些相异的地方呢?
父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处 : 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器)(定时器是以进程为单位进行分配,每个进程有且仅有一个) 6.未决信号集。
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后再映射至不同的物理内存吗?
当然不是!
父子进程间遵循 读时共享写时复制 的原则(针对的是物理地址)。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
实例
编写程序测试,父子进程是否共享全局变量?
父进程修改全局变量,子进程只读
#include<stdio.h> #include<unistd.h> // 定义一个全局变量 int var = 10; int main(int argc,char **argv) { // 创建一个子进程 pid_t pid = fork(); if(pid == 0) { // son sleep(1); printf("var = %d,child,pid = %d,ppid = %d.\n",var,getpid(),getppid()); } else if(pid > 0) { // parent printf("var = %d,parent,pid = %d,ppid = %d.\n",var,getpid(),getppid()); var = 11; printf("var = %d,parent,pid = %d,ppid = %d.\n",var,getpid(),getppid()); } return 0; }
子进程修改全局变量,父进程只读
#include<stdio.h> #include<unistd.h> // 定义一个全局变量 int var = 10; int main(int argc,char **argv) { // 创建一个子进程 pid_t pid = fork(); if(pid == 0) { // son printf("var = %d,child,pid = %d,ppid = %d.\n",var,getpid(),getppid()); var = 11; printf("var = %d,child,pid = %d,ppid = %d.\n",var,getpid(),getppid()); } else if(pid > 0) { // parent sleep(1); printf("var = %d,parent,pid = %d,ppid = %d.\n",var,getpid(),getppid()); } return 0; }
结论:
全局变量修改后,父子进程不再共享全局变量。
父子进程间遵循 读时共享写时复制 的原则。
exec函数族说明
fork函数用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。
使用命令来查看下 man execl
重点掌握execl和execlp这两个函数。
execlp比execl多个’p’,这个p指的是PATH
看一下函数的返回值。
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
原理
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,然后从新程序的启动例程开始执行
(换内容,不换包装)
execlp函数及测试代码:
翻译:
这些函数的初始参数是要执行的文件的名称。
在execl(), execlp()和execle()函数中,const char* arg和其后的省略号可以被认为是arg0, arg1,…, argn。它们一起描述了一个由一个或多个指针组成的列表,这些指针指向以空结束的字符串,这些指针表示被执行程序可用的参数列表。按照约定,第一个参数应该指向与正在执行的文件相关联的文件名。参数列表必须以一个空指针结束,由于这些是可变函数,这个指针必须强制转换为(char *) null。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int arg,char **argv) { //int execlp(const char *file, const char *arg, .../* (char *) NULL */); //const char *file 对应要执行的程序 //arg:参数列表 //返回值:只有失败才返回. // /* (char *) NULL */ NULL是说参数列表最后要加一个NULL,作为结尾的标志. execlp("ls","ls","-l",NULL);//第一个参数应该指向与正在执行的文件相关联的文件名。 //不需要判断返回值 perror("exec err"); exit(1); return 0; }
打印结果比较:
从输出结果中看以看出,有颜色的区别。
实际上,我们手动执行“ls -l“是在执行ls --color=auto -l。
为什么呢?
ls --color=auto 是 ls的别名。
通过 alias 命令可用于查看并设置指令的别名。
我们输入alias ls ,
所以说ls --color=auto 是 ls的别名。
我们现在想通过运行可执行程序来达到同样的效果,只需要加入参数即可。
运行一下。
故意做错:
将参数或文件名故意写错。
execl函数及测试代码:
execl的函数参数只比execlp多了一个路径PATH
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int arg,char **argv) { //int execlp(const char *file, const char *arg, .../* (char *) NULL */); //const char *file 对应要执行的程序 //arg:参数列表 //返回值:只有失败才返回. // /* (char *) NULL */ NULL是说参数列表最后要加一个NULL,作为结尾的标志. //execlp("ls","ls","--color=auto", "-l",NULL);//第一个参数应该指向与正在执行的文件相关联的文件名。 execl("/bin/ls","ls","--color=auto", "-l",NULL); //如果不出错的话不会返回,验证下。 printf("Test.\n"); //不需要判断返回值 perror("exec err"); exit(1); return 0; }
除了上面提到的两个函数之外,还有另外三个函数没有讲解。
事实上,只有execve是真正的系统调用,其它五个函数在运行过程中最终都会调用execve(系统调用),所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
首先,前面已经介绍了execl的功能,我们要理解当我们在控制台中输入ls命令的时候,-bash又是怎么执行的呢?
使用execl时,在指定文件路径后,可以执行自己写的程序。
孤儿进程:父进程死了,子进程被init进程领养。
僵尸进程:子进程死了,父进程没有回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸进程。
写一段孤儿进程的代码:
#include<stdio.h> #include<unistd.h> int main(int argc,char **argv) { // 创建一个子进程 pid_t pid = fork(); if(pid == 0) { while(1) { // son printf("child,pid = %d,ppid = %d.\n",getpid(),getppid()); sleep(1); } } else if(pid > 0) { // parent printf("parent,pid = %d,ppid = %d.\n",getpid(),getppid()); sleep(3); printf("I am parent,I will die.\n"); } return 0; }
运行一下:
从图中可以看出,CTRL+C不好使了。为什么?
因为子进程被init领养了,已经脱离-bash进程(shell)了,所以不好使了。
只能采用kill来杀死。
写一段僵尸进程的代码:
#include<stdio.h> #include<unistd.h> int main(int argc,char **argv) { // 创建一个子进程 pid_t pid = fork(); if(pid > 0) { while(1) { // father printf("father,pid = %d,ppid = %d.\n",getpid(),getppid()); sleep(1); } } else if(pid == 0) { // son printf("child,pid = %d,ppid = %d.\n",getpid(),getppid()); sleep(2); printf("I am child,I will die.\n"); } return 0; }
运行一下:
查看进程
看图中的第二行,
Z+(Z就代表僵尸进程的状态)
[jiangshi] ,
defunct:adj. 非现存的,失灵的,不再使用的;死的
说明该进程已经死了。
那么又怎么将僵尸进程的资源回收?
一种方法是杀死父进程,然后子进程(僵尸进程)被init进程领养,最后由init进程负责回收。
还有一种方法是父进程可以调用wait或waitpid来彻底清除掉这个进程,从而回收资源。
那这到底是怎么回事呢?我们接着往下看。
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。僵尸进程是不能用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止了。
父进程调用wait函数可以回收子进程终止信息。
我们使用
来查看下。
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdio.h> 4 #include<sys/types.h> 5 #include<sys/wait.h> 6 #include<stdlib.h> 7 8 int main(int argc,char **argv) 9 { 10 // 创建一个子进程 11 pid_t pid = fork(); 12 if(pid > 0) 13 { 14 // father 15 printf("parent,pid = %d,ppid = %d.\n",getpid(),getppid()); 16 // 不关心它的状态,使用NULL表示 17 // 成功返回回收子进程的pid,失败则返回-1. 18 // pid_t wait_id = wait(NULL); 19 // 怎么得到子进程终结的原因呢? 20 // 定义一个int 型的变量来接收 21 int status; 22 // 传入status的地址 23 pid_t wait_id = wait(&status); 24 printf("wait ok,wait_id = %d.\n",wait_id); 25 26 // 如果WIFEXITED(status)为真,说明是正常死亡 27 if(WIFEXITED(status)) 28 { 29 printf("the reason of child die is %d.\n", WEXITSTATUS(status)); 30 } 31 if(WIFSIGNALED(status)) 32 { 33 printf("the reason of child die is %d signal.\n", WTERMSIG(status)); 34 } 35 while(1) 36 { 37 sleep(1); 38 } 39 } 40 else if(pid == 0) 41 { 42 // son 43 printf("son,pid = %d,ppid = %d.\n",getpid(),getppid()); 44 sleep(3); 45 printf("I am child,I will die.\n"); 46 //return 101; 47 exit(202);//需要使用到头文件stdlib.h 48 } 49 return 0; 50 }
输出结果如下:
看下还有僵尸进程吗
确实成功回收掉了。
(给子进程加个while循环,保证它处于运行状态,然后,用kill命令来杀。)
在上述代码的基础上进行修改。
40 else if(pid == 0)
41 {
42 // son
43 printf("son,pid = %d,ppid = %d.\n",getpid(),getppid());
44 sleep(3);
45 while(1)
46 {
47 printf("I am child,I will not die forever.\n");
48 sleep(1);
49 }
50 //return 101;
51 exit(202);
52 }
输出结果如下:
另启动一个终端,然后在该终端中使用kill命令。
然后,会在第一个终端的运行窗口看到:
除了使用9号信号之外,还可以使用哪种信号来杀呢?
我们使用kill直接加pid号来试一下,看看打印出来的是哪种信号杀的呢?
使用15号信号来杀的。
看看都有哪些kill信号。
使用kill -l命令来查看。
先来看函数原型中的第一个参数pid_t pid。
pid的值可以是:
< -1 意思是等待进程组ID等于pid绝对值的任何子进程。(这样能回收一个组的任何死亡的子进程)
-1 意思是等待任何子进程。
0 意思是等待进程组ID等于调用进程组ID的任何子进程。
> 0 意思是等待进程ID等于pid值的子进程。
这里更多的是使用-1。
来说一说这个进程组,怎么看进程组呢?前面也提到了,使用 ps ajx|进程名。
第三列PGID就代表着进程组。进程组号相同的几个进程处于同一个进程组。
再来看第二个参数 int *status,这个参数和wait函数的status的参数含义一样,当status被指定为NULL,表示不关心子进程是如何死的。
对于第三个参数,这里只关注WNOHANG。
前面的wait函数是阻塞等待,也就是只有子进程死了之后,程序才接着往下执行。
当options参数选项中选择WNOHANG时,waitpid函数不会阻塞等待,也就是直接返回。
当options参数选项中选择0时,也就和wait函数没有太大的区别了。会阻塞等待。
最后,再来关注下返回值。
waitpid():成功时,返回状态已经改变的子进程的进程ID;如果指定了WNOHANG,并且pid指定的一个或多个子进程存在,但状态尚未改变,则返回0。出错时,返回-1。
什么情况下会出错呢,当所有子进程都死了(不存在)且都被回收了,会返回-1.
会在程序中进行验证。
写代码验证下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid =fork(); if(pid==0) { printf("I am child,pid=%d.\n",getpid()); sleep(2);//2秒后子进程将死去。 } else if(pid>0) { printf("I am parent,pid=%d.\n",getpid()); //回收所有子进程,-1 //不问子进程是怎么死的,NULL. //当option的值为0时,表示阻塞等待,等待子进程死亡后返回,与wait函数无异。 //当option的值为WNOHANG时,表示不等子进程死亡就立刻返回。 int ret = waitpid(-1,NULL,WNOHANG); //返回值: //成功时,返回状态已经改变的子进程的进程ID; //如果指定了WNOHANG,并且pid指定的一个或多个子进程存在,但状态尚未改变,则返回0。 //出错时,返回-1。 printf("ret = %d.\n",ret); while(1) { sleep(1); } } return 0; }
运行程序:
ret = 0说明,指定了WNOHANG,pid指定的一个或多个子进程存在,但状态尚未改变,所以返回0。此时子进程没有被回收成功。但子进程死去了,也就是说会产生僵尸进程。
使用ps aux|grep waitpid_test 来查看waitpid_test进程相关信息。
果然,产生了僵尸进程。
下面来修改代码,死等子进程死去。然后回收成功,以及所有子进程死去,然后再回收,回收失败。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid =fork(); if(pid==0) { printf("I am child,pid=%d.\n",getpid()); sleep(2); } else if(pid>0) { printf("I am parent,pid=%d.\n",getpid()); //回收所有子进程,-1 //不问子进程是怎么死的,NULL. //当option的值为0时,表示阻塞等待,等待子进程死亡后返回,与wait函数无异。 //当option的值为WNOHANG时,表示不等子进程死亡就立刻返回。 int ret; while((ret= waitpid(-1,NULL,WNOHANG))==0) { sleep(1); } //返回值:表示 printf("ret = %d.\n",ret); //当程序执行到这里的时候,所有的子进程都已经被回收了 ret=waitpid(-1,NULL,WNOHANG); printf("ret = %d.\n",ret); if(ret<0) { perror("wait error"); } printf("ret = %d.\n",ret); while(1) { sleep(1); } } return 0; }
运行程序:
再使用ps aux|grep waitpid_test来查看waitpid_test进程相关信息。
没有僵尸进程出现。
#include<stdio.h> #include<unistd.h> #include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.h> int main(int argc,char **argv) { int i=0; pid_t pid; for(i=0;i<5;i++) { pid = fork(); if(pid == 0) { printf("son_%d,pid = %d.\n",i,getpid()); break; } } sleep(i); if(i<5) { printf("I am child,will exit,pid = %d,ppid = %d.\n",getpid(),getppid()); } else if(i==5) { for(i=0;i<5;i++) { pid_t wait_id = wait(NULL); printf("wait son_%d,wait_id =%d.\n",i,wait_id); } } return 0; }
#include<stdio.h> #include<unistd.h> #include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.h> int main(int argc,char **argv) { int n=5; int i=0; pid_t pid; for(i=0;i<5;i++) { pid = fork(); if(pid == 0) { printf("son_%d,pid = %d.\n",i,getpid()); break; } } sleep(i); if(i<5) { printf("I am child,will exit,pid = %d,ppid = %d.\n",getpid(),getppid()); } else if(i==5) { printf("I am parent.\n"); // 如何使用waitpid回收? // waitpid return -1代表子进程都死了,都收了。 // 此时就退出循环。 while(1) { pid_t wait_id = waitpid(-1,NULL,WNOHANG); if(wait_id == -1) { // waitpid return -1代表子进程都死了,都收了。 // 此时就退出循环。 break; } //如果wait_id == 0,会contine,接着执行循环体。 else if(wait_id>0) { printf("wait_id =%d.\n",wait_id); } } } return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。