Linux进程基础(二)
孤儿进程
概念:父进程运行结束,但子进程还是运行(未结束运行)的子进程就称孤儿进程。
孤儿进程是没有父进程的进程,为避免孤儿进程退出时无法释放所占用的资源而变为僵尸进程,进程号为 1 的 init 进程将会接受这些孤儿进程,这一过程也被称为“收养”。init 进程就好像是一个孤儿院,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
示例演示:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid;
//创建一个进程
pid = fork();
//创建失败
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
if (pid == 0)
{
printf("I am the child process.\n");
//输出进程ID和父进程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
//睡眠5s,保证父进程先退出
sleep(5);
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
}
//父进程
else
{
printf("I am father process.\n");
//父进程睡眠1s,保证子进程输出进程id
sleep(1);
printf("father process is exited.\n");
}
return 0;
}
运行结果如下:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023095610712-1536791364.png)
僵尸进程
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
特征:
- 僵死的时候,task_struct是会被保留的,进程的退出信息是放在PCB中的
- 父进程没有读取子进程的状态信息,子进程就会进入僵死状态
- 父进程读取子进程状态码后,子进程会由Z状态变成X状态
危害:
- 进程的退出状态必须被维持下去,父进程如果一直不读取,那子进程就一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费。
- 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
如何解决?
僵尸进程产生的原因是父进程不回收子进程资源,并且父进程不结束,子进程也无法被init进程收养,所以会造成子进程一直占用内存的资源,解决方法就是杀死产生僵尸进程的元凶。通过发送SIGTERM或者SIGKILL信号杀死进程,杀死掉进程之后,僵尸进程就会被init进程收养,释放掉他们占用的资源。
通过信号机制解决僵尸进程:
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
static void sig_child(int signo);
int main()
{
pid_t pid;
//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
static void sig_child(int signo)
{
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}
运行结果如下:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023095736819-97227654.png)
可以看到没有出现僵尸进程。
进程优先级
概念:
- cpu资源分配的先后顺序,就是指进程的优先权
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023095819519-1437803033.png)
几个重要信息:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI 和NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
- 当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
修改进程优先级
注意:
- NI的值的范围是-20到19
- 普通用户调整NI的值的范围是0到19,而且只能调整自己的进程
- 普通用户只能调高NI值,而不能降低,比如原来的NI值是0,只能调整NI大于0
- root用户才能设定进程NI的值是负数,而且可以调整任何用户的进程
- 用户只能修改NI的值,而不能直接修改PRI
修改优先级的命令:
1.nice [选项] 命令
nice命令可以给新执行的命令直接赋予NI值,但是不能修改已经存在进程的NI值。
选项:-n NI值:给命令赋予NI值
例如:nice -n -10 ws_gw 设置进程ws_gw的优先级的修正值为-10
2.renice [优先级] PID
renice命令是修改已经存在进程的NI值的命令
例如:renice -10 2125
进程程序替换
fork创建子进程后一般会有两种行为:
- 想让子进程执行父进程的一部分代码(可以理解为子承父业)
- 想让子进程执行和父进程完全不同的代码,也就是程序替换(可以理解为儿子创业)
程序替换的原理:
1.将磁盘中的程序,加载入内存结构
2.重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程)
- 子进程的PCB结构并没有改变,只是改变页表映射关系。
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023095850923-1618197564.png)
思考1:程序替换的本质是什么?
把磁盘中的程序的代码和数据用加载器加载进特定的进程的上下文中,底层用到了exec系列的程序替换函数
思考2:程序替换后,有没有新进程被创建?
答案是没有的。因为进程替换前后,没有创建新的PCB、虚拟内存和页表等数据结构,也就是进程的这些数据结构没有发生变化,进程替换只是对物理内存中的数据和代码进行了修改,前后进程的ID没有发生改变,所程序替换不创建新进程
思考3:子进程发生程序替换后,代码和数据都发生写时拷贝吗?
由于进程替换会把新程序的代码和数据加载到特定的进程,为了让父子进程之间具有独立性,修改的代码和数据都要发生写时拷贝,这样才不会影响父进程的数据和代码
替换函数
#include <unistd.h>
extern char **environ;
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 execve(const char *path, char *const argv[], char *const envp[]);
函数返回值: 调用出错返回-1,没有调用成功的的返回值
函数参数:
- path:用来替换的程序所在的路径
- file:程序名
- arg, …:列表的形式传参
- arg[]:数组的形式传参
- envp[]:自己维护的环境变量
函数名解释:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
函数的使用方法:
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 是 | 是 |
execlp | 列表 | 否 | 是 |
execle | 列表 | 是 | 否,自己组装环境变量 |
execv | 数组 | 是 | 是 |
execvp | 数组 | 否 | 是 |
execve | 数组 | 是 | 否,自己组装环境变量 |
函数调用案例如下:
int main()
{
// 自己组装的环境变量
char* myenv[] = {"MYENV=you can see my", NULL};
// 列表形式传参
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execp("ls", "ls", "-l", "-a", NULL);
exece("./mycmd", "mycmd", NULL, myenv);
// 数组形式传参
char* const argv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls",argv);
execvp("ls", argv);
char* const agrv1[] = {"mycmd", NULL};
execve("./mycmd", agrv1, myenv);// 调用自己的程序
}
环境变量&进程程序替换
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,它是系统中某些具有全局性质的变量,通常是为了满足某些系统的需求。
环境变量=变量名+变量内容(路径)
常见的几个环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100247185-1131005885.png)
可以用echo $NAME 命令来查找的当前的环境变量。
为什么要有环境变量?
系统的全局变量,都是为了方便用户、开发者和系统进行某种最简单的查找和定位,确认等等问题。
举个例子
通常,我们在使用ls、pwd这些指令时,我们是不需要带上路径的,而我们运行一个程序时,我们需要带上路径,这是为什么呢?
看下面的图,我们可以看到,ls这个指令的路径被写进环境变量PATH中了,所以,当我们执行ls这个指令时,系统会在环境变量PATH下挨个搜索路径,找到就执行,没找到就报错。
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100116313-1952659158.png)
环境变量组织方式
每个程序都会有一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,这些字符串存放的就是换变量。
获取环境变量的三种方式:
(1)命令行第三个参数
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[], char *env[])
{
for (int i = 0; env[i]; ++i)
{
printf("%d:%s\n", i, env[i]);
}
return 0;
}
运行结果如下:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100341969-586286455.png)
(2)通过第三方变量environ获取
#include<stdio.h>
int main()
{
extern char **environ;
int i=0;
for(i;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时,要用extern声明。
(3)系统调用获取或环境变量
常用putenv和getenv函数来访问特定的环境变量
用man手册查看getenv函数的用法:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100406261-1640175340.png)
那么如何通过getenv函数查找当前的PATH呢?
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[], char *env[])
{
printf("%s\n",getenv("PATH"));
return 0;
}
运行结果如下:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100435671-1406229193.png)
总结: 环境变量可以被子进程继承下去,因为环境变量通常具有全局属性。
程序替换
了解完环境变量之后,我们重新区分下进程的程序替换的六个函数。
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100459312-1392502517.png)
- 首先,这六个函数全部都已exec四个字符开头。
- 函数名中含字符’l’的不会含字符’v’,它们的区别是函数名中含有‘l’的表示列举参数,意思就是如果我们要调用”ls -al”指令,就需要将所有参数通过字符串的形式列举出来,即arg=“ls”,”-a”,”-l”,NULL;函数名中含有‘v’表示参数向量表,意思是我们要把参数写在一个指针数组中,例如上面的“ls -al”,我们可以定义一个指针数组char* const agr[]={“ls”,”-a”,”-l”,NULL,}然后向含有字符’v’的函数名的函数传参;我们需要注意的是不管是列举参数,还是参数向量表,最后一个参数都必须是NULL。
- 还有一个区别是字符’p’,函数名含有字符’p’的函数不需要指定路径(绝对路径和相对路径都可以),exec函数会通过PATH环境变量在指定的各目录中搜寻可执行文件;而不含字符’p’的函数需要指定可执行文件的路径。
- 最后一个区别是字符’e’,函数名的最后一个字符是‘e’的函数可以接收一个指向环境字符串指针数组的指针,而其他的函数则使用调用进程中的environ变量为新程序复制现有进程的环境变量。
举个例子:
![](https://img2022.cnblogs.com/blog/2976263/202210/2976263-20221023100514016-1533751261.png)