进程控制
进程创建——fork函数
用于创建子进程的系统调用接口!
这是一个函数函数执行前只有一个父进程,函数执行后有一个父进程一个子进程
进程调用fork,当控制转移到内核中的fork代码后,内核做
- 分配新的==内存块和内核数据结构(PCB,地址空间,页表)==给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork后的子进程和父进程的数据使用写实拷贝的方式各自私有
但是进程的代码是各自私有的!可以使用if else的方式进行分流!
#include<stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0 )
{
perror("fork");
return 1;
}
else if(id == 0)
{
while(1)
{
printf("我是子进程 ,pid = %d ,ppid = %d\n",getpid(),getppid());
sleep(1);
}
//child
}
else
{
//parent
while(1)
{
printf("我是父进程 ,pid = %d ,ppid = %d\n",getpid(),getppid());
sleep(3);
}
}
return 0;
}
关于fork的返回值
如何理解fork返回之后,给父进程返回子进程的pid 给子进程返回 0 ?
fork常规用法
- 一个父进程希望复制自己,==使父子进程同时执行不同的代码段==。例如,父进程等待客户端请求,生成子 进程来处理请求。
- 一个进程==要执行一个不同的程序==。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制 可以写一个死循环自己测试!
进程终止
退出码
我们写c/c++的时候,往往会在的最后写一个return 0;
int main()
{
return 0;
}
为什么要返回 -0?
语言上我们称这个返回0 ,但是在系统上我们称为==进程退出的试试,对应的退出码!==
==退出码的意义是用来标定进程执行的结果是否正确!==
==strerror函数就是c语言提供的将退出码转换成字符串描述的一个函数!==
#include <stdio.h>
#include <string.h>
int main()
{
for(int i =0;i<135;i++)
{
printf("%d : %s\n",i,strerror(i)); }
retrun 0;
}
==可以发现每一个错误都有一个错误的原因!==——我们日常在linux下面使用的指令也是c语言的写的一个程序!其退出码和转换出相应的错误表达就是使用的是strerror!
退出的情况
进程退出有这几种情况!
1.代码跑完了,结果正确——return 0;
2.代码跑完了,结果正确——return 非0//退出码就是在这情况起作用!
3.代码没跑完,程序异常了——这时候退出码就没有意义了!
进程如何退出
1.main函数返回!
2.任意地方调用exit!
进程等待
进程等待的必要性
linux进程有一种状态!——Z状态(僵尸状态)这个状态是为了子进程终止后,等待父进程/操作系统来获取它的退出的信息!
但是有时候父进程一直执行不退出,导致了子进程一直处于僵尸状态,占用系统资源!此时连kill -9这个指令都没有办法,所以我们必须使用一些办法来释放僵尸状态!——我们可以通过进程等待方式来用解决僵尸进程的问题!
父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程可以通过进程等待的方式,==来回收子进程资源==,==获取子进程的退出信息!==——这就是进程为什么要等待的原因
进程等待的方法
wait函数
wait如果等待成功,返回的就是子进程的pid,如果等待失败就会返回 -1
#include <stdio.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 10;
while(cnt)
{
printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid()); --cnt;
sleep(1);
}
exit(0);
}
sleep(15);
pid_t ret =wait(NULL);
if(id > 0)
{
printf("wait succeeful :%d\n",ret);
}
sleep(5)
}
等到子进程执行完毕后!还要等待父进程5秒钟才能回收!——这5秒的时间都是在等待父进程醒来的僵尸状态,然后就只剩下父进程了!
waitpid
pid_t waitpid(pid_t pid,int* status,int optios);
这个函数的如果成功返回这个子进程的pid ,如果失败返回 -1,如果子进程没有改变状态放回 0
第一个参数 pid_t pid就是要等待的子进程的pid!
第三个参数是用来说明在什么时候等待!——0就表示在阻塞时候等待!
第二分个参数 status是一个输出型参数!==就是通过等待拿到子进程的退出结果==!
#include <stdio.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进&status,0程
int cnt = 5;
while(cnt)
{
printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());
--cnt;
sleep(1);
}
exit(10);
}
int status = 0;
pid_t ret =waitpid(id,&status,0);
if(id > 0)
{
printf("wait succeeful :%d,ret :%d\n",ret,status);
}
sleep(5);
return 0;
}
为什么ret的值为什么是2560呢?
==原因是因为status这个值不是整体被使用的!==——是按照特定的位图结构来进行设置不同的值!
WIFEXITED(status)
==WIFEXITED(status)==: 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status)
==WEXITSTATUS(status)==: 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进&status,0程
int cnt = 5;
while(cnt)
{
printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());
--cnt;
sleep(1);
}
exit(10);
}
int status = 0;
pid_t ret =waitpid(id,&status,0);
if(ret > 0)
{
if(WIFEXITED(status)) //判断进程是否正常退出!
{
printf("exit code :%d \n",WEXITSTATUS(status)); //获取进程退出信息!(获得退出码!)
}
else
{
printf("child exit no normal!\n");
}
}
sleep(5);
return 0;
}
总结!
-
进程退出变成僵尸!——会把自己的退出结果写入到自己的task_struct里面去!
-
wait/waitpid是一个系统调用!——是以OS的身份去读取子进程的task_struct
-
即退出的时候操纵系统要从子进程的task_struct里面读取退出信息!
-
task_struct内核里面的退出信息!
非阻塞等待
上面的方法如果子进程一直不退出!那么==父进程就必须一直卡在哪里==等待着它退出——这就是==阻塞式等待!==
如果我们在某一次突然发现子进程没有像我们想预想的一样退出,但是我们==又不想等待想要继续执行父进程后面的代码==,只是想==时不时查询看一下子进程到底退出了没有!==——这既是==非阻塞等待!==
非阻塞等待的意义!
在非阻塞等待的是,不会占用父进程的所有的精力!
可以在轮询期间做一些其他的事情!
例如下面的例子!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 10
typedef void(*func_t)();//函数指针!
func_t handlerTask[NUM];//函数指针数组!
void task1()
{
printf("handler task1\n");
}
void task2()
{
printf("handler task2\n");
}
void task3()
{
printf("handler task3\n");
}
void loadTask()
{
memset(handlerTask,0,sizeof(handlerTask));
handlerTask[0] = task1;
handlerTask[1] = task2;
handlerTask[2] = task3;
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());
--cnt;
sleep(1);
}
exit(10);
}
//parent
loadTask();
int status = 0;
while(1)
{
pid_t ret = waitpid(id,&status,WNOHANG);//非阻塞状态!
if(ret == 0)
{
//子进程没有退出,waitpid没有等待失败!仅仅是检测到子进程没有退出!
printf("wait done but the child is runing .....\n");
sleep(1);
for(int i = 0;handlerTask[i] != NULL;i++)
{
handlerTask[i]();//采用回调的方式,执行我们想让父进程在空闲的时候做的事情!
}
}
else if(ret > 0)
{
//waitpid调用成功!且子进程退出!
printf("wait succeeful :%d,sign number : %d,child exit code :%d\n",ret,(status & 0x7F),((status >> 8)& 0xFF) );
break;
}
else
{
//waitpid调用失败!
printf("waitpid call failed\n");
break;
}
}
return 0;
}
进程程序替换——重点
创建子进程的目的是什么呢?
- 想让子进程执行父进程代码的一部分!——就是执行父进程对应的磁盘代码的一部分!
- ==想让子进程执行一个全新的程序!==——让子进程加载磁盘上指定的程序!执行程序的代码和数据!==这就是进程的程序替换!==
替换函数——初识execl
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
==函数的作用就是将程序加载到内存中,让指定进程进行执行!==
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("procee is running\n");
//加载程序!
execl("/usr/bin/ls"/*找到程序*/,"ls","--color=auto","-a","-l",NULL);
//所有exec的接口都必须以NULL结尾!表示传参结束!
printf("process is done\n");
return 0;
}
==成功的调用了ls命令!==——但是奇怪了为什么 没有process is done 打印出来呢?
这就要说到进程替换的原理了!
进程替换原理
程序替换的本质就是将==指定的程序的代码和数据加载到指定的位置!==(指定的位置就是用来覆盖进程自己的代码和数据!)
==进程替换的时候没有创建新的进程!==——因为没有创建新的PCB!
所以我们就可以解释为什么process is done 没有被打印出来了!
因为调用exec函数后原来的代码和数据全都被覆盖掉了!原来的printf指令也是属于代码和数据!
被覆盖掉了!自然就无法执行了!
如果函数调用失败!——即没有替换成功!那么就不会发生替换!
exec系列的函数只会出错的时候返回!——成功之所以不返回!是因为一旦函数执行成功本进程的后续所有代码就被替换了!都没有意义了!
进程替换演示
一般进程替换我们都是使用子进程来调用其他程序!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
sleep(1);
execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
==我们发现子进程的程序替换是不会影响父进程的!==——进程具有独立性!
==因为一旦使用fork之后父子进程都会有自己的task_struct!,然后一旦调用exec函数就会导致子进程的数据发生改变,从而发生写实拷贝!在内存里面给子进程重新开辟一个新的物理内存!而父进程的数据不受任何影响!==
替换函数——重点
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);
除了上面的哪一个替换函数还有剩下4种替换函数!
int execl(const char *path, const char *arg, ...);
上面我们使用的这个函数后面的 l就是list的的意思——是指将指令参数一个个的传过去!
execlp
int execlp(const char *file, const char *arg, ...);
p:就是path
带p字符的函数不用告诉其程序的路径!只要说程序是谁!——==那么这个函数就会在环境变量PATH里面去进行可执行程序的查找!==
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
sleep(1);
execlp("ls","ls","-a","-l","--color=auto",NULL);//第一个ls是程序名!第二个ls是指令!
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
==程序照样正常执行!==
execv
int execv(const char *path, char *const argv[]);
v的意思是vector的意思!——相比execl可以将所有的执行参数都统一放入一个指针数组中!进行统一的传递!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
sleep(1);
char* const argv_[]{
"ls",
"-a",
"-l",
"--color=auto",
NULL
};//也要以NULL结尾!
execv("/usr/bin/ls",argv_);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
execvp
int execvp(const char *file, char *const argv[]);
和上面的带p的一样就是可以不带路径!
这个exec函数会自动的在环境变量中找程序,同时使用的是v的数组传参!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
sleep(1);
char* const argv_[]{
"ls",
"-a",
"-l",
"--color=auto",
NULL
};//也要以NULL结尾!
execv("ls",argv_);//不用找路径!
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
虽然刚刚我们演示的都是系统自带的指令!
但是我们也可以去调用我们自己写的程序!
execl("./mybin","mybin",NULL);//使用我们自己的的程序路径!
==而且这个程序替换不仅仅可以执行相同语言的程序!==
==其他语言写的的程序也是可以可以执行的!==——因为替换的时候是系统给我们进行替换的!
==像是java/Python写的程序都是可以调用的!==
==我们可以使用程序替换,调用任何语言对应的可执行文件!==
execle
int execle(const char *path, const char *arg, ...,char *const envp[]);
e就是自定义环境变量!
我们可以在执行我们的进程的时候将环境变量给传进去!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
sleep(1);
char* const envp_[] = {
"MYENV=112233445566"
};//我们在自己进程里面定义的环境变量!
execle("./mybib","mybin",NULL,envp_);
extern char** envron;
//execle("./mybib","mybin“,NULL,envron);//也可以传系统给我们的环境变量!
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
//mybin.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("PATH:%s\n",getenv("PATH"));
printf("PWD:%s\n",getenv("PWD"));
printf("MYENV:%s\n",getenv("MYENV"));
printf("这是我的程序!\n");
printf("这是我的程序!\n");
printf("这是我的程序!\n");
printf("这是我的程序!\n");
printf("这是我的程序!\n");
printf("这是我的程序!\n");
return 0;
}
上面的是通过execle调用的进程!因为使用的是我们自己定义的环境变量!
下面的mybin使用的是系统自带的环境变量!
==我们也可以使用putenv将我们自己的变量导入到环境变量里面!==
putenv((char*)"MYENV=44332211");
extern char** environ;
execle("./mybib","mybin“,NULL,envron);//也可以传系统给我们的环境变量!
execve
这个是真正的系统调用接口!
属于的是2号手册——即系统调用的接口!
==上面调用的所有接口本质都是这一个接口的封装!目的是为了给我们更多选择更加方便的使用!==
exec函数总结
在linux中将程序加载到内存中的都是使用的是exec*系列的接口!——这些接口又称为加载器!
==我们在命令行上面使用 ./xxx 来运行程序也是通过调用这一系列的接口的!==
==main函数的三个参数的都是由exec这个接口传参过来了的!==
==上面的exec函数有的虽然没有env参数,但是可以通过eviron这个第三方变量来获取环境变量!==
exec函数的应用
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,char* argv[])
{
printf("process is running....\n");
pid_t id = fork();
if(id == 0)
{
execvp(argv[1],&argv[1]);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
}
return 0;
}
我们可以在后面带上各种指令!——通过这一点我们就可以自己写一个简易的shell!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#include <string.h>
#define NUM 1024
#define OPT_NUM 64
char LineCommand[NUM];
char* myargv[OPT_NUM];
int main()
{
while(1)
{
//打印输出提示符
printf("用户名@主机名 当前路径# ");
fflush(stdout);//刷新缓冲区!
//获取用户输入,我们输入的时候会自己输入\n
char* s = fgets(LineCommand,sizeof(LineCommand)-1,stdin);
(void)s;
assert (s != NULL);
LineCommand[strlen(LineCommand)-1] = 0;//这是为了清楚最后一个\n!
//"ls -a -l" -> "ls" "-a" "-l" 切割字串!
myargv[0] = strtok(LineCommand," ");
int i = 1;
if(myargv[0] != NULL && strcmp(myargv[0],"ls") ==0)
{
myargv[i++] = (char*)"--color=auto";//自动给ls加上颜色
}
//strtok遇到空的返回值为NULL 同时要myargv要以NULL结尾!
while(myargv[i++] = strtok(NULL," "));
//执行命令!——让子进程执行!
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
execvp(myargv[0],myargv);
exit(1);
}
waitpid(id,NULL,0);
}
}
上面写的简易的shell有个问题!那就是当我们使用cd的时候却无法切换路径!
这是为什么呢?——首先我们要理解一个概念==”当前路径“==
什么叫当前路径?我们可以先写一个小进程来进行测试
在linux里面,当一个进程运行的时候,会自动的在/proc/下面形成以这个进程的pid为名字的目录!
这个文件里面包含了进程的所有属性!
==那么这个cwd是可以修改的吗?==——可以系统为我们提供了一个函数
chdir
这个函数的作用就是更改当前的工作路径(cwd)!
使用也很简单!只要把要切换到的路径的给当做参数传上去就可以!
#include <stdio.h>
#include <unistd.h>
int main()
{
chdir("/home/xxx");
while(1)
{
printf("我是一个进程!:%d\n",getpid());
sleep(1);
}
return 0;
}
我们可以看到cwd成功的切换了!
==那我们自己的shell为什么cd的时候路径没有发生变化呢?==