linux进程控制
Posted 可乐不解渴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux进程控制相关的知识,希望对你有一定的参考价值。
记忆的梗上,谁不有两三朵娉婷,披着情绪的。
linux进程控制
进程创建
我们在上一篇进程概念中已经讲过一些进程的创建,在这里我们简单叙述一遍。
fork函数
创建子进程的最为关键的是fork()函数,在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
函数原型:pid_t fork(void);
其实这个pid_t类型原型其实就是unsigned int。
该函数包含在#include<unistd.h>
中。
我们的进程调用fork,当控制转移到内核中的fork代码后,内核做:
1、首先先分配新的内存块(PCB)和内核数据结构给子进程;
2、再将父进程部分数据结构内容(如页表、进程地址空间)拷贝至子进程;
3、其次添加子进程到系统进程列表当中;
4、最后fork返回,使用调度器调度。
fork函数返回值
fork函数会返回两个返回值。
- 一个给父进程返回的是子进程的pid;
- 另一个返回0值给子进程。
写时拷贝
通常在不写入时,父子代码和数据都是共享的。但当任意一方试图写入,便以写时拷贝的方式使得各自拥有一份副本。
为什么会存在写时拷贝呢?
通常情况子进程再不写入的情况下,如果直接给子进程直接拷贝一份和父进程完全相同的代码和数据时,势必会导致数据的冗余和内存空间的可使用量减少,因为父子进程的数据和代码都一样,那么直接让子进程使用父进程的代码和数据即可,直到要修改时才进行拷贝。而OS使用写时拷贝是因为为了减少内存空间的使用,更加合理的分配内存空间。
fork失败的通常情况
1、系统中有太多的进程,内存空间不足导致创建失败。
2、实际用户创建的进程数量超过了限制。
进程终止
通常情况下我们的进程退出场景有三种:
1、代码出现异常然后终止。
2、代码运行完毕,结果正确。
3、代码运行完毕,结果不正确。
而在我们进程退出的方法也有三种:
- 从main中使用return返回退出码。
- 调用exit返回退出码。
- _exit系统调用返回退出码。
- 进程出现异常终止ctrl + c,信号终止。
_exit与exit
这两个函数都包含在#include<unistd.h>
中
首先我们先说_exit系统调用接口,该接口函数原型为:void _exit(int status);
其中这个status定义了进程的终止状态。
下面我们简单的来使用一下:
#include<iostream>
using namespace std;
void test()
{
cout<<"hello CSDN"; //故意不换行,不去主动刷新缓冲区
_exit(-1);
}
int main()
{
test();
return 0;
}
该代码是让main函数去调用test()函数让其执行其内部的两行代码。
在上面的动图中我们会发现使用了_exit函数并没有将我们的hello CSDN打印出来,这是为什么呢?
因为我们上面的代码是没有使用换行的,所以没有让缓冲区进行行刷新策略,而执行_exit函数直接将该进程直接终止,导致我们想要打印的话直接丢失掉了。
而exit函数与_exit函数不同的事,它会帮我们将缓冲区里的内容刷新。
还是使用上面的代码,但是函数换成了exit。
#include<iostream>
using namespace std;
void test()
{
cout<<"hello CSDN"; //故意不换行,不去主动刷新缓冲区
exit(-1);
}
int main()
{
test();
return 0;
}
在上面的图片中我们可以发现使用了exit函数它会将我们缓冲区内的数据刷新出来。
进程等待
在直接我们讲僵尸进程的时候说过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。而父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是错,或者是否出现异常导致不正常退出。此时父进程就需要通过进程等待的方式,来回收子进程资源,获取子进程退出信息。
进程等待的方法
进程等待的方式有两种,分别是wait和waitpid。
而它们所在的头文件为#include <sys/types.h>和#include <sys/wait.h>
wait
首先我们先说说wait。其函数原型为:pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。这里的status与我们用C语言刷力扣,与上面的函数形参的returnsize是一个道理。
下面我们运用代码来做测试:
#include<iostream>
#include<unistd.h>
#include <sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
pid_t id=fork();
if(id<0)
{
cerr<<"creater process error"<<endl;
}
else if(id==0)
{
sleep(5);
cout<<"I am child, pid: "<<getpid()<<" ppid:"<<getppid()<<endl;
}
else
{
pid_t ret=wait(NULL);
cout<<"I am father, pid: "<<getpid()<<" ppid:"<<getppid()<<endl;
if(ret>0)
{
cout<<"return child pid:"<<ret<<endl;
}
}
return 0;
}
在上面的代码和最后的结果中,我们发现wait成功返回了子进程的pid,这表明等待子进程退出成功。
waitpid
waidpid的函数原型为:pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
1、当正常返回的时候waitpid返回收集到的子进程的进程ID。
2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:
pid:
pid=-1,等待任一个子进程。与wait等效。
pid>0,等待其进程PID与pid相等的子进程。
status:
WIFEXITED(status):
若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)。
WEXITSTATUS(status)
: 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)。
status不能简单的当作整形来看待,可以当作是数据结构中的位图来看待,具体细节如下图(只研究status低16比特位):这里它是用整形的2个字节来实现的,高2个字节不看。每一位对应不同的状态。
如果进程是正常终止的话,那么我们怎么得到它的退出码呢?
我们可以利用位操作得到。如:(status>>8)&0xff
,而判断该进程是否是正常退出只需要利用status&0x7f
来判断。
options:
WNOHANG: 以非阻塞的方式等待。若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。
进程的阻塞方式等待:
阻塞的方式意思就是直到成功才返回。举个例子,比如你找你的好朋友去吃饭,你到了他家楼下等他下楼,那么只有等到他下来,你们才能一起去吃饭。
#include<iostream>
#include<unistd.h>
#include <sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
pid_t id=fork();
if(id<0)
{
cerr<<"creater process error"<<endl;
}
else if(id==0)
{
sleep(5);
cout<<"I am child id"<<endl;
}
int status=0;
//我们这里以阻塞的方式等待子进程的退出
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
//WIFEXITED(status)
if((status&0x7f)==0)
{
//WEXITSTATUS(status)
cout<<"return code:"<<((status>>8)&0xff)<<endl;
}
}
return 0;
}
进程的非阻塞方式等待:
而非阻塞的方式意思是:每隔一小会来检测一次进程退出没有,如果没有先返回0代表子进程还在运行,其先去执行别的代码,过会再来检查进程退出没有。
还是上面那个例子:你和你朋友去吃饭,朋友叫你在楼下等他,你每隔一会就打他个电话问他好了没有,如果没有你是不是可以做别的事情,比如刷抖音、听听歌之类,之后会了一定时间的间隔在继续打朋友的电话问他好了没有,一直重复进行类似这种操作。
#include<iostream>
#include<unistd.h>
#include <sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
pid_t id=fork();
if(id<0)
{
cerr<<"creater process error"<<endl;
}
else if(id==0)
{
int count=5;
while(count--)
{
sleep(1);
cout<<"I am child id"<<endl;
}
exit(0);
}
while(1)
{
int status=0;
//以非阻塞的形式去等待子进程的退出,每隔一会来看看子进程退出没有
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret>0)
{
if((status&0x7f)==0)
{
cout<<"return code:"<<((status>>8)&0xff)<<endl;
break;
}
}
else if(ret==0)
{
sleep(1);
cout<<"child has running"<<endl;
}
}
return 0;
}
执行过程如下:
进程程序替换
在这之前我们都只是创建子进程,然后让子进程去执行父进程的部分代码,相当于代码执行的是同一份代码。如果我们不想让子进程执行一丁点与父进程有关的代码呢?这就需要进行进程程序替换了,让子进程去执行另一个与父进程没有任何关系的程序。
而我们要将一个可执行程序运行起来变成进程,第一件是一定是创建PCB,然后构建页表,第二件事是将对应的代码和数据加载到内存当中,那么最后将两份资源进行对接即可。那么我们可不可以不要父进程里的东西,而是直接从磁盘中重新加载一个全新的程序,将新的程序的代码和数据加载给子进程,相当于是子进程在进行写时拷贝,将父进程的代码和数据覆盖掉。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
在使用前我们得先了解一下程序替换函数有如下几个:
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[]);
这些函数的头文件都为#include <unistd.h>
。
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,否则就是成功的,它将不会执行父进程的代码,所以exec函数只有出错的返回值而没有成功的返回值。
exec系列函数命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表。
v(vector) : 参数用数组。
p(path) : 有p自动搜索环境变量PATH。
e(env) : 表示自己维护环境变量。
下面我们自己来写一些代码来运用这些函数。
exec系列函数使用
execl
由于该函数名字上没有带p,所以第一个参数需要我们手动的添加要执行的程序的路径,第二个参数开始表示我们想怎么执行这个程序,最后我们必须保证以NULL结尾。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process\\n");
sleep(3);
execl("/usr/bin/ls","ls","-al",NULL);
printf("error\\n");
return 0;
}
执行结果如下:我们会发现execl函数下的printf并没有被打印,这就表明是替换成功的,如果替换失败了,那么就会执行printf这行代码。
execlp
由于这个函数名带p,所以我们就不需要给具体的路径了,它可以使用环境变量PATH,无需写全路径。这里我们就可以写相对路径,后面的参数和之前一样想怎么执行这个程序。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process\\n");
sleep(3);
execlp("ls","ls","-al",NULL);
printf("error\\n");
return 0;
}
结果和刚刚的一样,如下图所示:
execle
这个函数名称又带l又带e,所以按照上面的理解,e表示我们需要自己去给环境变量。
这里我们用该函数去调用自己写的另一个c++程序。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process\\n");
sleep(3);
char*myenv[]={"myenv=youcanseeme",
"PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ZJ/.local/bin:/home/ZJ/bin",
NULL};
execle("./cmd","cmd",NULL,myenv);
printf("error\\n");
return 0;
}
其中上面调用getenv来获取环境变量,其中从父进程的execlp函数传过来的myenv会将系统的环境变量覆盖掉。然后我们想要打印某个名字环境变量必须是传进来里名称有的,如果没有则会输出null。=号前面即是环境变量的名称。
结果如下图所示:
execv
在上面我们运用的都是带l的,由于有l就不可能会有v,在这里是互斥的,并且函数名并没有带p所以我们必须将要执行的程序的路径写全。
然后运用一个char类型的指针数组在内部存储我们要怎么执行这个程序,然后将这个数组的名称放入函数的参数中即可。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process\\n");
sleep(3);
printf("I am a execv\\n");
char *arr[]={"ls","-al",NULL};
execv("/usr/bin/ls",arr);
printf("error\\n");
return 0;
}
结果如下图所示:
execvp
同理带p就可以不需要写绝对路径了,当然写全也不会有问题。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process\\n");
sleep(3);
printf("I am a execvp\\n");
char *arr[]={"ls","-al",NULL};
execvp("ls",arr);
printf("error\\n");
return 0;
}
结果如下图所示:
在上面我明明写了有6个exec系列函数但我偏偏将他们分开两种格式来写。因为在只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它的函数在man手册第3节。而这些函数之间的关系如下图所示:
如果小伙伴还没看懂可以在评论区留言,我会在评论区给你解答!
如有错误之处还请各位指出!!!
那本篇文章就到这里啦,下次再见啦!
以上是关于linux进程控制的主要内容,如果未能解决你的问题,请参考以下文章