首页 > 系统相关 >[ Linux ] 进程控制

[ Linux ] 进程控制

时间:2022-10-28 19:31:28浏览次数:45  
标签:控制 int Linux pid printf 进程 include id

0.进程创建

相比大家对下面这段代码已经不陌生了,我们在介绍fork()的时候就已经写过一遍了,fork()有两个返回值,同一个pid会有不同的值,这是上篇我们说到的伪内存问题。而本篇我们要看看fork()创建时,操作系统会干什么事情?

#include <unistd.h> 
pid_t fork(void);

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
while(1)
{
printf("我是父进程,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}

[ Linux ] 进程控制_进程替换

当一个进程调用fork之后,就有两个二进制代码相同的进程,而且他们都运行在相同的地方。但是每个进程都将可以开始他们自己的旅程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("我是一个进程:pid = %d\n",getpid());
fork();
printf("我依旧是一个进程:pid = %d\n",getpid());
return 0;
}

[ Linux ] 进程控制_进程等待_02

当我们运行完时发现,fork()之后代码共享。

fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行。

那么fork()之后是否之后fork()之后的代码被父子进程共享的??

结论:一般情况下,fork()之后,父子共享所有的代码 ,因此fork()之后,父进程共享了全部的代码,只不过子进程只能从fork开始执行。子进程继承了父进程的eip(程序计数器),但是如果子进程想找到之前的代码也是可以的。

0.1fork()之后,操作系统做了什么?

我们都知道进程=内核的进程数据结构+进程的代码和数据。当fork()创建的时候是创建子进程的内核数据结构(struct tast_struct + struct mm_struct... + 页表) + 代码继承父进程,数据以写实拷贝的形式来共享或者独立!因此,fork()之后,操作系统创建结构,代码以共享的形式,数据以是写实拷贝的形式来实现两个进程整体保持独立性!也就是说,父进程或者子进程如果有一方进程挂掉,不会影响另一方。

0.2写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

[ Linux ] 进程控制_进程控制_03

那么为什么要写时拷贝?在创建子进程的时候就把数据分开不行吗?

答案是不行的,具体原因有一下三点:

  1. 父进程的数据,子进程不一定全用,即使使用,也不一定全部写入 ,因此会有浪费空间的嫌疑
  2. 最理想的情况,只有会被父子修改的数据进行分离拷贝,不需要修改的共享即可--但是从技术角度实现很复杂
  3. 如果fork的时候,就无脑拷贝数据的子进程,会增加fork的成本(内存和时间)

所以最终采用写时拷贝。只会拷贝父子修改的数据,变相的就是拷贝数据的最小成本,但是拷贝的成本依然存在。之所以写时,是因为这是延迟拷贝的策略,只有真正使用的时候操作系统再给你分配资源。因此这种写时拷贝变相的提高内存的使用率。

0.3fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

1.进程终止

1.1关于终止的认识

我们在写C/C++程序的时候,每个程序都有一个main函数,这个函数也叫做入口函数。我们经常会习惯的写上

return 0 ,那么这里将会产生两个问题:

  • return 0 给谁返回?
  • 必须返回0吗?返回别的数字可以吗?

此时我们首先要了解到进程退出的场景:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序异常了

任何一个程序无外乎这三种退出场景,本篇文章主要介绍前两种场景。我们将举一个例子来加什么对这两种场景的认识和理解:假设张三要参加期末考试,他无非就3中情况:1.正常参加考试,考了100分。2.正常参加考试,考了20分。3.为正常参考,原因也多种多样。类比到这里,我们也能够理解,一个程序也无外乎这三种情况,代码跑完,结果正确;代码跑完,结果不正确;代码未跑完,程序发生异常。就好比我们写一个排序算法要将一组数据进行排序要么代码跑完,排序成功;要么程序跑完,排序失败;要么程序都压根没跑完。

我们用0表示sucess结果正确,非0表示结果失败。(非0标识不同的原因也不同)

因此retun X(X叫做进程退出码)进程退出码表征了进程退出的信息,这个进程退出信息将来要将父进程读取的。因此这个退出信息码非常的重要。因此我们这里回答了return是给父进程返回

我们写一段程序验证一下

#include <stdio.h>

int main()
{
return 123;
}

[ Linux ] 进程控制_进程控制_04

当我们运行这个代码的时候,该进程的父进程是bash,因此这个程序的退出码我们可以使用bash下的命令echo来查看退出码

echo $?

[ Linux ] 进程控制_进程替换_05

1.1.1$?

这个$?表示在bash中,最近一次执行完毕时,对应进程的退出码!当我们再查看一次的时候发现是0,大家也不要觉得奇怪。这是因为在shell看来,echo $?这条命令也被当成是一个进程(虽然他不是),因此就会变成了0

[ Linux ] 进程控制_进程控制_06

1.1.2进程退出码

在我们刚刚说正常退出 进程退出码是0 0表示success,那么异常退出的时候其他的退出码都表示什么含义呢?

比如这里看一个ls 跟上一串随机字符,我们查看退出码就为2(非0)

[ Linux ] 进程控制_进程等待_07

因此,一般而言,失败的零值该如何设置呢?以及默认表达的含义?这里我们大家也不需要刻意记忆每个进程退出码对应的含义,因为我们可以自定义来设置,或者用的时候查一查就行。那么我们现在看看系统的代码是什么含义,我们可以使用strerror函数进行查看(下图为man帮助手册查看的strerror的作用及其用法)

[ Linux ] 进程控制_进程替换_08

#include <stdio.h>
#include <string.h>
int main()
{
int i = 0;
for(;i<100;++i)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}

[ Linux ] 进程控制_进程控制_09

我们在这里大概看几个,我们看到0表示success,1表示权限不允许(可执行程序),2表示找不到文件

[ Linux ] 进程控制_进程替换_10

因此我们可以得出结论:不同的进程退出码可以对应不同错误原因。

1.2进程终止的常见做法

一般我们有两种做法最常见:

  1. 在main函数中return,代表进程结束,非main函数return表示函数调用结束,为什么其他函数不行呢?
  2. 在自己的代码中任意地点中,调用exit(),即使非main函数也可以退出

1.2.1exit

我们来看看exit的用法

[ Linux ] 进程控制_进程控制_11

我们写一段简单的程序看看

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void fun()
{
printf("fun()\n");
exit(20);
}
int main()
{
fun();
return 123;
}

我们执行这段代码,我们通过查看进程退出码可以确定该程序是从exit推出的还是return出去的。通过结果我们可以看到,程序是从exit退出的。

[ Linux ] 进程控制_进程替换_12

因此如果以后我们想终止一个进程,可以在想终止的地方调用exit()。

1.2.2_exit

这里我们之所以介绍_exit仅仅是因为他和我们刚刚介绍的exit长得很像,我们在这里也不需要特别记忆_exit的用法。在此处,我们就简单介绍一下_exit如何使用,以及_exit和exit的区别。

我们通过查看_exit发现,_exit是一个系统调用,其实exit调用了_exit。

[ Linux ] 进程控制_进程控制_13

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void fun()
{
printf("fun()\n");
_exit(123);
}
int main()
{
fun();
return 20;
}


[ Linux ] 进程控制_进程控制_14

此时我们发现_exit和exit好像没有什么区别,实际上他俩还是有区别的,我们来看看下面这段代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(1);
exit(111);
return 20;
}

我们首先使用exit来提前终止进程,我们查看结果发现hello world能够被刷新出来

[ Linux ] 进程控制_进程控制_15

而当我们在调用_exit时,显示器什么也没输出。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(1);
_exit(111);
return 20;
}

[ Linux ] 进程控制_进程等待_16

结论:exit终止进程会刷新缓冲区 _exit终止进程不会刷新缓冲区(_exit我们就了解这么多)

1.3关于终止,内核做了什么?

进程 = 内核结构 + 进程代码和数据

当进程终止时,代码和数据一定会被释放掉。对于内核结构(tast_struct && mm_struct),操作系统可能并不会释放该进程的内核数据结构。

2.进程等待

2.1为什么要进程等待

  1. 解决僵尸进程问题 -- 解决内存泄漏的问题
  2. 解决获取子进程的退出状态问题。首先:一个进程理应该获得子进程的退出状态。在今天,我们只讨论父进程必须通过进程等待的方式获取子进程的退出状态。

2.2进程等待的必要性

  1. 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。因此我们需要想办法让该进程由Z状态变为X状态。
  2. 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

2.3如何等待--进程等待的方法

2.3.1wait方法

我们使用 man 手册查看wait方法

[ Linux ] 进程控制_进程控制_17

我们刚才说到了进程等待可以解决将进程由僵尸状态变成释放状态,我们写一段代码来验证一下

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
}
}
else
{
//parent
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
sleep(40);
pid_t ret = wait(NULL);
if(ret<0)
{
printf("等待失败\n");
}
else
{
printf("等待成功: result: %d\n",ret);
}
sleep(20);
}
return 0;
}

这段代码,我们可以预期看到3个现象:

  1. 子进程由运行变成Z状态
  2. Z状态在等待成功时就没了
  3. result的返回值等于子进程的pid

我们写一段监控监本代码来实时查看这个过程

while :; do ps ajx | head -1 
&& ps ajx | grep wait | grep -v grep;
echo"--------------------------------------------";
sleep 1;done

[ Linux ] 进程控制_进程等待_18

以上就是wait的基本调用。我们使用wait()的方案可以解决子进程Z状态,让子进程进入X状态。

2.3.2 waitpid

[ Linux ] 进程控制_进程等待_19

[ Linux ] 进程控制_进程控制_20

wait的作用是等待任意一个退出的进程,而接下来我们重点介绍一个waitpid(). wait()/waitpid() 是系统调用!!

返回值pid_t:

>0 : 等待子进程成功,返回值就是子进程的id

<0:等待失败

第一个参数pid:

>0:是几,就代表等待拿一个子进程,pid = 1234 ,等待1234进程,因此就是等待指定进程

-1: 等待任意进程

第二个参数status:
  • status是什么:

这个参数,是一个输出型参数,通过调用这个函数,从函数内部拿出来特定的数据。因为status是一个整形指针,因此拿出来的一定是要一个整数。从内部拿出来的数据就是从子进程的进程控制块(tast_struct)中拿出子进程退出的退出码!

  • status的构成

我们关于status只需要关心改整数的低16个比特位。这16个比特位会分为3个部分。次低8位(8-15)存放这子进程的退出码

[ Linux ] 进程控制_进程替换_21

关于低7位的作用,我们刚刚说到,代码跑完结果正确,代码跑完,结果不正确,那么代码异常呢?因此低7位的作用就是处理异常。一个进程如果异常退出,是因为这个信号收到了特定的信号!!

[ Linux ] 进程控制_进程替换_22

第三个参数options:

0 : 我们可以先设置为0,0表示阻塞等待。什么意思呢?可以理解为父进程在等待回收子进程,但是子进程就是不返回呢,那么父进程就等待受阻了,这时父进程就阻塞等待了。

2.3.3waitpid()验证

验证status次低8位是子进程退出码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
cnt--;
if(!cnt)
{
break;
}
}
exit(20);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d\n",ret,(status>>8)&0xFF);
}
}
}

[ Linux ] 进程控制_进程等待_23

我们假设这里把退出码换成31-->exit(31);

[ Linux ] 进程控制_进程等待_24

此时我们已经成功的获得了子进程的退出码。

但是我们发现使用位操作的成本是比较高的,因此linux给我提供了一些宏来直接可以调用

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

因为我们可以改写一下等待代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(5);
break;
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
//是否正产退出
if(WIFEXITED(status))
{
printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
}
}
}
return 0;
}

[ Linux ] 进程控制_进程等待_25

验证status最低7位的作用 -- 处理异常
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
ret,(status>>8)&0xFF,status&0x7F);
}
}
}

这段代码子进程会一直死循环运行,在子进程运行期间,父进程在阻塞等待子进程。

[ Linux ] 进程控制_进程控制_26

我们可以使用kill -l 查看其他的进程信号

[ Linux ] 进程控制_进程替换_27


我们刚刚验证了 9号信号 我们再验证一个3号信号看看

[ Linux ] 进程控制_进程控制_28

通过这两个验证我们确实已经发现了status的低7位是存储的进程异常退出的信号。

其他信号的验证
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(5);
//除零异常
int a = 10/0;
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
ret,(status>>8)&0xFF,status&0x7F);
}
}
}

[ Linux ] 进程控制_进程等待_29

我们查看8号信号发现是浮点数异常

[ Linux ] 进程控制_进程等待_30


那父进程拿到了子进程的退出码和退出信号,父进程先看谁呢?

进程退出码对应的前两种进程退出:1.代码跑完,结果正确;2.代码跑完,结果不正确

而进程一旦出现异常,只需要关心退出信号,退出码没有任何意义

2.3.4 阻塞等待和非阻塞等待

阻塞等待:

当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待其实本质就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒。因此所谓的阻塞就是进程阻塞。

进程的非阻塞等待:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

将options设置为WNOHANG就非阻塞等待,如果是非阻塞等,子进程没有退出,返回值为0,等待失败,返回-1,等待成功,返回子进程pid

[ Linux ] 进程控制_进程控制_31

那么我们将代码改成非阻塞等待

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(3);
}
exit(0);
}
else
{
//parent
int status = 0;
while(1)
{
pid_t ret = waitpid(-1,&status,WNOHANG);
if(ret>0)
{
printf("等待成功,%d,exit sig:%d,exit code:%d\n",
ret,status&0x7F,(status>>8)&0xFF);

}
else if(ret == 0)
{
//等待成功了 但是子进程没有退出
printf("子进程好了吗,还没,那么父进程做其他事情.....\n");
sleep(1);
}
else{
//出错了 暂时不处理
}
}
}
}

以上就是非阻塞轮询检测。

[ Linux ] 进程控制_进程等待_32

使用kill -9 pid 杀掉子进程,此时父进程等待成功,获取子进程pid以及退出信号

[ Linux ] 进程控制_进程替换_33


3.进程程序替换

3.1进程程序替换是什么?(概念,原理)

子进程执行的是父进程的代码片段,如果我们想让创建出来的子进程,执行全新的程序呢?想让父子进程彻底分开!

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变.

[ Linux ] 进程控制_进程等待_34

[ Linux ] 进程控制_进程等待_35

程序替换的原理:将磁盘中的程序加载入内存结构,重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程),效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!!!

3.2为什么要让子进程执行一个新的程序呢?

我们一般在服务器设计(linux编程) 的时候,往往需要子进程干两件种类事情:

  1. 让子进程执行父进程的代码片段(服务器代码)
  2. 让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程,执行别人写的进程代码等等),C/C++,python,shell,java......

3.3 怎么做(编码,如何进行程序替换)

3.3.1 见一下最基本的代码

进程替换函数

其实有六种以exec开头的函数,统称exec函数:

#include <unistd.h>`
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[]);

[ Linux ] 进程控制_进程等待_36

int execve(const char *path, char *const argv[], char *const envp[]);

[ Linux ] 进程控制_进程替换_37

如果想要执行一个全新的程序(本质就是磁盘上的文件),我们需要做几件事情

  1. 先找到这个程序在那里?
  2. 程序可能携带选项进行执行(也可以不携带),明确告诉OS,我想要怎么执行这个程序

1.execl

[ Linux ] 进程控制_进程控制_38

其中参数列表中:"..." 叫做可变参数,说白了就是可以按照用户的意愿传入参数的大小个数,如果还不理解,大家肯定都用过C语言中的printf函数吧,printf有没有规定你只能打印几个参数呢?没有的,这是根据用户自己来定义的!这就是可变参数

[ Linux ] 进程控制_进程替换_39

execl 第一个参数 path: 就是告诉OS,这个程序在哪里。

execl 第二个参数:就是告诉OS,我想怎么执行。

我们使用C语言来看看,我们发现调用了ls -l -a 的命令

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("我执行完毕了,我的pid是:%d\n",getpid());
return 0;
}

[ Linux ] 进程控制_进程控制_40

我们再调用一个top命令看看

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
//execl("/usr/bin/ls","ls","-l","-a",NULL);
//top
execl("/usr/bin/top","top",NULL);
printf("我执行完毕了,我的pid是:%d\n",getpid());
return 0;
}

[ Linux ] 进程控制_进程替换_41

这个就是程序替换!但是在程序替换的过程中,发生了什么问题呢?我们发现最后一行代码没有打印!

答案是因为一旦execl替换成功,是将当前进程的代码和数据全部替换了!!后面的printf是代码,是代码已经被替换了,该代码就不存在了!

所以这个程序替换函数用不用判断返回值? 为什么?

int ret = execl(....);

execl是一个程序替换,一旦替换成功了,还会执行返回语句吗?不会了,因为程序替换成功之后没机会了,那么这里的程序替换的返回值有意义吗?答案依然是有意义的,因为替换成功才不会执行,那么失败的时候,必然会继续向后执行,可以通过返回值得到什么原因导致的替换失败!

那我们依然用代码来看看替换失败的时候

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
return 0;
}

[ Linux ] 进程控制_进程控制_42

3.3.2 引入进程创建

我们刚刚写的程序并没有创建子进程,都是自己替换自己,而我们有时候就想让子进程做这个事情,那么我们把进程创建进入进来,我们修改一下代码

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}



// ls -a -l
//execl("/usr/bin/ls","ls","-l","-a",NULL);
//top
//int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
//printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
return 0;
}

[ Linux ] 进程控制_进程替换_43

通过这个例子我们得到新的结论:子进程执行程序替换,不会影响父进程的程序,因为进程具有独立性!因此子进程无论怎么替换都不会影响父进程!

那么请问如何做到的?子进程和父进程如何在代码和数据上做分离的呢?当时我们说数据层面上发生写实拷贝!那么么代码不是共享的吗,父进程不会发生变化吗?实际上,当程序替换的时候,我们可以理解为,代码和数据都发生了写实拷贝完成父子的分离!

3.3.3大量的测试各种不同的接口

我们刚刚测试了一下execl接口,还有其他的接口,我们挑几个来测试一下。

[ Linux ] 进程控制_进程控制_44

[ Linux ] 进程控制_进程控制_45

execv

[ Linux ] 进程控制_进程等待_46

execv第一个参数和execl一致,而第二个参数是一个指针数组,这个指针数组指向的是什么?

我们也很好理解,刚刚execl是将一个一个char* 传入函数内,而execv是把这些char*整成一个数组,最后把这个指针数组传入execv即可

我们通过代码测试一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char* const argv_[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
NULL
};
execv("/usr/bin/ls",argv_);

//execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}

[ Linux ] 进程控制_进程控制_47

因此我们发现execl和execv这两个接口其实并没有很大的区别,只是传入参数的方式不同而已!

execlp

[ Linux ] 进程控制_进程控制_48

第一个参数:你想执行什么程序 -- 找到它 我们执行指令的时候,默认的搜索路径是path,这里的p就是表示的path,因此带p的可以不带路径,只说出你要执行哪一个程序即可

第二个参数:如何执行它。

我们通过代码来验证一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execlp("ls","ls","-a","-l",NULL);
//这里出现的两个ls,可以省略吗,含义一样吗?
//不能省略 含义不一样
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}

[ Linux ] 进程控制_进程控制_49


execvp

[ Linux ] 进程控制_进程控制_50

有了上面几个的铺垫,我们再看这个接口就特别的好理解了,第一个参数在Path找,第二个接口传入一个指针数组。我们也来验证一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char* const argv_[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
NULL
};
execvp("ls",argv_);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}

return 0;
}

[ Linux ] 进程控制_进程替换_51


execle

[ Linux ] 进程控制_进程替换_52

这里的前两个接口都非常熟悉了,这里最后一个接口叫做环境变量。那么为什么要有这个接口呢?

说到环境变量之前我们先来看一下这个问题,我们刚刚提到过,进程替换可以让我们执行其他语言写的程序,那么我们怎么来执行呢?(我们使用execl 函数来调用)

[ Linux ] 进程控制_进程替换_53

我们现在的目标是想用我们写的myproc.c把mycmd.cpp调用起来,那么怎么来用呢?

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("/home/Lxy/code/linux-code/practice/10-28/mycmd",/*使用绝对路径的方式*/
"mycmd",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
#include <iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}

[ Linux ] 进程控制_进程替换_54

我们发现,我们成功的用我们程序调用了cpp文件,我们刚刚使用的是绝对路径,我们也可以使用相对路径,只需要将路径修改为相对路径即可

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("./mycmd","mycmd",NULL);//使用相对路径的方式
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}

[ Linux ] 进程控制_进程控制_55

我们再运行发现,仍然是可以成功的。当然也可以用我们的程序来调用python语言,shell脚本语言等等,在这里我就不写代码了,将我自己的实验截图放在此处。

[ Linux ] 进程控制_进程等待_56


[ Linux ] 进程控制_进程控制_57

测试这么多,我们知道了任何程序都可以用系统级接口调用其他语言的

谈完这个话题我们再来谈谈环境变量,execle这个函数多了一个e,这个e就是环境变量,如果你想给这个函数传入环境变量,我们就可以传入环境变量。

首先我们先来传入一个系统存在的环境变量,我们使用myproc.c程序调用这个cpp文件

#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
cout<<"PATH:"<<getenv("PATH")<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}

[ Linux ] 进程控制_进程等待_58

发现没有任何问题,那么如果我们想 传入一个自己手动写的环境变量呢?我们就可以使用execle函数了

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char *const env[] = {
(char*)"MYPATH=youcanseeme!",NULL };
//e:添加环境变量给目标进程是覆盖式的
execle("./mycmd","mycmd",NULL,env);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
//cout<<"PATH:"<<getenv("PATH")<<endl;
cout<<"---------------------------------\n";
cout<<"MYPATH:"<<getenv("MYPATH")<<endl;
cout<<"---------------------------------\n";
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}

[ Linux ] 进程控制_进程控制_59


讲完这些接口我们发现还剩下的其他接口参数大致相同,我们掌握上面几个接口之后,我们就可以使用其他的接口!至此,进程控制结束,我们可以简易实现一个shell

(本篇完)

标签:控制,int,Linux,pid,printf,进程,include,id
From: https://blog.51cto.com/xingyuli/5805257

相关文章

  • 实验6:开源控制器实践——RYU
    (一)基本要求1.搭建下图所示SDN拓扑,协议使用OpenFlow1.0,并连接Ryu控制器,通过Ryu的图形界面查看网络拓扑。1)构建topo:sudomn--topo=single,3--mac--controller=remot......
  • 实验6:开源控制器实践——RYU
    实验6:开源控制器实践——RYU一、实验目的能够独立部署RYU控制器;能够理解RYU控制器实现软件定义的集线器原理;能够理解RYU控制器实现软件定义的交换机原理。二、实验......
  • Linux 配置java环境
    封寝时间长了,想搞个minecraft服务器和同学们玩玩,结果安上java版我的世界服务端后居然报错,说Java版本不够。。然后直接重装了整个云服务器的系统,打算重新装一个Java17来。......
  • 实验4:开源控制器实践——OpenDaylight
    实验4:开源控制器实践——OpenDaylight一、实验目的能够独立完成OpenDaylight控制器的安装配置;能够使用Postman工具调用OpenDaylightAPI接口下发流表。二、实验环境......
  • Golang基础-流程控制
    流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块控制语句分为三大类:顺序、选择、循环一、分支结构if分支结构单分支......
  • linux nginx记录
     安装nginx前首先安装四个依赖包--以下命令一键安装四个依赖 yum-yinstallgcczlibzlib-develpcre-developensslopenssl-devel 创建指定目录下载安装ngi......
  • linux出现中文乱码,怎么处理
    linux出现中文乱码,如下所示处理方法:第一步:打开.bash_profile文件,vim ~/.bash_profile第二步:在文件末尾添加这两行,exportLC_ALL="zh_CN.GBK"exportLANG="zh_CN.GBK"第......
  • linux 中查找过去一段时间内被修改过的文件
     001、[root@pc1test]#lsa.txtb.txtc.txt[root@pc1test]#ll-h##测试文件total4.0K-rw-r--r--.1rootroot0Oct2816:36a.txt-rw-r......
  • Linux 中 find命令实现按照多个条件查找文件
     001、[root@pc1test]#lsa.txtb.txtc.txtd.txte.txtm.mapn.mapx.csvy.csv[root@pc1test]#find./-name"*.map"##查找当前目录中所......
  • Linux系统下如何设置开机自动运行脚本?以Redis开机自启为例
    参考链接:https://baijiahao.baidu.com/s?id=1722174560616569543&wfr=spider&for=pc前言:最近在安装Redis的时候,想找下Redis开机自启的功能,在网上找了很多的教程,貌似很多......