Linux系统编程之进程介绍

本文详细介绍了Linux系统中的进程管理,包括进程概述、监控进程的`ps`和`top`命令、进程标识码、`fork`与`vfork`函数、进程的退出机制、进程等待、僵尸进程与孤儿进程,以及`exec`族函数和`system`、`popen`函数的使用。通过实例展示了如何创建、监控和管理进程,帮助读者深入理解Linux进程的运作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

达者为先  师者之意


1 进程的概述

Linux系统编程——进程
进程是程序的动态的执行过程。程序是静态的过程
程序: 存储在磁盘上的一堆指令。
进程: 将程序动态的运行起来,就转成进程,(即程序的执行过程)过程: 包含进程的创建,进程的运行以及进程消亡 。

  • Linux系统是一个多进程的系统,它的进程之间具有互不干扰等行性、特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
  • 当创建一个进程,系统会分配0—4G 虚拟地址空间,其中0—3G是用户空间 3—4G内核空间 。多个进程共用同一份内核, 各自进程的用户空间是独立的 。
  • 空间的划分是由内核来决定,0—3G用户空间3—4
    内核空间,这个划分是内核默认 。

在这里插入图片描述

  • 代码段: 存放字符常量
  • 数据段: 初始化后的全局变量 只读数据段: const 所修饰的变量
  • bss数据段: 未被初始化的全局变量
  • 堆:程序员自己手动申请的空间 malloc -------- 小栈 大堆 思想 。
  • 栈:存放函数内部所包含的局部变量以及函数。

进程的类型:

  • 交互进程 :shell 终端 就是交互进程 。
  • 批处理进程 :通过 ps –aux 来进行查看到的进程 。
  • 守护进程 :随系统的启动开始运行,系统关闭的时候,结束运行,在运行期间不会停止运行。在linux 操作系统下面守护进程不受终端的控制 。

2 ps命令

ps 命令是最常用的监控进程的命令,通过此命令可以查看系统中所有运行进程的详细信息。

ps 命令有多种不同的使用方法,这常常给初学者带来困惑。在各种 Linux 论坛上,询问 ps 命令语法的帖子屡见不鲜,而出现这样的情况,还要归咎于 UNIX 悠久的历史和庞大的派系。在不同的 Linux 发行版上,ps 命令的语法各不相同。为此,Linux 采取了一个折中的方法,即融合各种不同的风格,兼顾那些已经习惯了其它系统上使用 ps 命令的用户。

ps 命令的基本格式如下:

[root@localhost ~] # ps aux
#查看系统中所有的进程,使用 BS 操作系统格式
[root@localhost ~] # ps -le
#查看系统中所有的进程,使用 Linux 标准命令格式

选项:

  • a:显示一个终端的所有进程,除会话引线外;
  • u:显示进程的归属用户及内存的使用情况;
  • x:显示没有控制终端的进程;
  • -l:长格式显示更加详细的信息;
  • -e:显示所有进程;
    可以看到,ps 命令有些与众不同,它的部分选项不能加入"-“,比如命令"ps aux”,其中"aux"是选项,但是前面不能带“-”。

大家如果执行 “man ps” 命令,则会发现 ps 命令的帮助为了适应不同的类 UNIX 系统,可用格式非常多,不方便记忆。所以,我建议大家记忆几个固定选项即可。比如:

  • “ps aux” 可以查看系统中所有的进程;

  • “ps -le” 可以查看系统中所有的进程,而且还能看到进程的父进程的 PID 和进程优先级;

  • “ps -l” 只能看到当前 Shell 产生的进程;

  • 注意:还可以用 grep [选项] [文件或目录]…过滤/搜索的特定字符
    如"ps aux|grep init" 查找 init 字段的进程

有这三个命令就足够了,下面分别来查看。
在这里插入图片描述
表1 罗列出了以上输出信息中各列的具体含义
在这里插入图片描述

"ps aux"命令可以看到系统中所有的进程,"ps -le"命令也能看到系统中所有的进程。由于 “-l” 选项的作用,所以 “ps -le” 命令能够看到更加详细的信息,比如父进程的 PID、优先级等。但是这两个命令的基本作用是一致的,掌握其中一个就足够了。
在这里插入图片描述
表 2 罗列出以上输出信息中各列的含义
在这里插入图片描述

如果不想看到所有的进程,只想查看一下当前登录产生了哪些进程,那只需使用 “ps -l” 命令就足够了:
在这里插入图片描述
可以看到,这次从 pts/1 虚拟终端登录,只产生了两个进程:一个是登录之后生成的 Shell,也就是 bash;另一个是正在执行的 ps 命令。

3 top命令

ps 命令可以一次性给出当前系统中进程状态,但使用此方式得到的信息缺乏时效性,并且,如果管理员需要实时监控进程运行情况,就必须不停地执行 ps 命令,这显然是缺乏效率的。
为此,Linux 提供了 top 命令。top 命令可以动态地持续监听进程地运行状态,与此同时,该命令还提供了一个交互界面,用户可以根据需要,人性化地定制自己的输出,进而更清楚地了进程的运行状态。

top 命令的基本格式如下:
[root@localhost ~] #top [选项]

选项:

  • -d 秒数:指定 top 命令每隔几秒更新。默认是 3 秒;
  • -b:使用批处理模式输出。一般和"-n"选项合用,用于把 top 命令重定向到文件中;
  • -n 次数:指定 top 命令执行的次数。一般和"-"选项合用;
  • -p 进程PID:仅查看指定 ID 的进程;
  • -s:使 top 命令在安全模式中运行,避免在交互模式中出现错误;
  • -u 用户名:只监听某个用户的进程;

在 top 命令的显示窗口中,还可以使用如下按键,进行一下交互操作:

  • ? 或 h:显示交互模式的帮助;
  • P:按照 CPU 的使用率排序,默认就是此选项;
  • M:按照内存的使用率排序;
  • N:按照 PID 排序;
  • T:按照 CPU 的累积运算时间排序,也就是按照 TIME+ 项排序;
  • k:按照 PID 给予某个进程一个信号。一般用于中止某个进程,信号 9 是强制中止的信号;
  • r:按照 PID 给某个进程重设优先级(Nice)值;
  • q:退出 top 命令;

4 进程标识码

函数原型:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

在这里插入图片描述
pid_t 的实质是int类型的。
getpid()返回当前进程标识码,getppid()返回父进程标识。

5 fork与vfork函数

fork函数

我们都知道fork可以用于进程的创建,那首先我们来了解一下fork函数原型和基本的两种用法才真正的了解我们何时才会用到fork来进行进程的创建。

函数原型:

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

参数含义:无参传入,返回pid_t类型的数值。pid_t 的实质是int类型的。
fork函数被调用一次,但是返回两次
子进程返回的是0,而父进程返回值则是新子进程的进程ID。
注意:父进程返回值是新进程的进程ID
因为一个进程的子进程可以有多个,并且没有一个函数是一个进程可以获得其所有子进程的进程ID。父进程中没有记录子进程的pid。所以为了方便父进程知道和处理子进程,fork()返回最新子进程的pid。

用法一

  • 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。
    这在网络服务进程中是最常见的一种处理,父进程等待客户端的服务请求,当达到请求到达时,父进程调用fork,使子进程处理此请求。父进程则等待下一个服务请求到达。

用法二

  • 一个进程要执行一个不同的程序
    这个用法对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

vfork函数(尽量不要使用)

函数原型:

#include<unistd.h>
pid_t vfork(void)  // 返回值pid_t 的实质是int类型的
  1. 不进行空间拷贝(写时都不拷贝),子进程pcb直接指向父进程pcb指向的位置
  2. 父进程一定是等到子进程执行完毕以后在执行
  3. 必须调用exit 或exec系列的函数,否则就会出现段错误 4、任何一个系统上实现的vfork或多过少的都有问题,尽量 不要使用

vfork()注意:
子进程先执行,不能return返回,return是在堆栈上进行操作,return返回相当于压栈一次,出栈两次,即子进程释放了main的栈,父进程再去释放就形成了段错误。

总结:fork和vfork的区别

  • 调用fork后, 父子进程运行顺序不定,而vfork子进程一定先运行。
  • fork存在写时拷贝的机制,子进程拷贝父进程地址空间。 vfork子进程(在调用exec(进程替换)或者exit之前)共享父进程的地址空间(一改都改)。
  • vfork必须调用exit返回,否则会引发段错误。

6 进程的退出

Linux 下进程的退出分为正常退出和异常退出两种:

  • 正常退出
    a. 在main()函数中执行return 。
    b.调用exit()函数
    c.调用_exit()函数
  • 异常退出
    a.调用abort函数
    b.进程收到某个信号,而该信号使程序终止,比如Ctrl+c,进程收到某个信号,而该信号是程序中止。

但不管是哪种退出方式,系统最终都会执行内核中的某一代码。这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源。

几种退出方式的比较

  1. exit和return 的区别:
    exit是一个函数,有参数。exit执行完后把控制权交给系统
    return是函数执行完后的返回。renturn执行完后把控制权交给调用函数。

  2. exit和abort的区别:
    exit是正常终止进程
    abort是异常终止。

关于exit函数
函数原型

#include <stdlib.h>
void exit(int status);

exit函数的参数:当前进程要返回的退出码
注意:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其它的值表示出现了错误,进程非正常结束。

函数说明

  • 直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。exit(0)表示正常退出,exit(x)(x不为0)表示异常退出,这个x是返回给操作系统(包括UNIX,Linux,和MS DOS)的,以供其他程序使用。

关于_exit函数
函数原型

#include <unistd.h>
void _exit(int  status)

注意:_exit 函数的参数:当前进程要返回的退出码
status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其它的值表示出现了错误,进程非正常结束。
_Exit函数等效于 _exit函数。
函数说明

  • _exit函数会立即终止调用过程。属于该进程的任何打开的文件描述符都被关闭;进程的任何子进程都由init进程(初始化进程,进程ID:1)继承,进程的父进程将被发送一个SIGCHLD信号。值状态作为进程的退出状态返回给父进程,并且可以使用wait(2)系列调用之一收集。

exit()和_exit()函数的区别
exit和_exit函数都是用来终止进程的。当程序执行到exit或_exit时,系统无条件的停止剩下所有操作,清除各种数据结构,并终止本进程的运行。

  • exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中声明。 exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。
  • _exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
  • 调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr …). exit函数是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新流。
  • exit()函数与_exit()函数最大区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux的标准函数库中,有一种被称作“缓冲I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也给编程代来了一点儿麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。

exit 最后也会调用 _exit函数,但exit在中止进程的同时,还会刷新缓冲区、关闭流等
如下图:
在这里插入图片描述

7 进程等待

前面提到这么一个问题
父进程在忙,子进程结束了,但无人回收,这样就造成了“死亡”的子进程一直占用资源,这个时候的子进程被称为“僵尸进程”
为了解决这个问题,最初的思路是:让父进程停下,等待子进程执行完,然后回收子进程,清理掉的子进程ID。

进程等待的必要性

  • 子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄漏。

  • 进程一旦变成僵尸状态,就会刀枪不入,kill -9 强杀也无能为力,

  • 父进程派给子进程的任务完成的如何,父进程得知道,

  • 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。

注意 : 当子进程执行时, wait()可以暂停父进程的执行,使其等待。一旦子进程执行完,等待的父进程就会重新执行。如果有多个子进程在执行,那么父进程中的 wait()在第一个子进程结束时返回,恢复父进程执行。

进程等待的方法(如何让父进程进行进程等待):
wait函数和waitpid函数
wait函数:
函数原型

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int* status)

返回值 pid_t : 成功返回被等待进程的 PID,失败返回 -1。
参数 status: 参数是一个指针类型,但是该指针类型并不是要传递一个指针参数,而是一个输出型参数 ; 获取子进程的退出状态,不关心则可以设置为 NULL。调用函数时不用传入具体的值,在函数内部赋予值,在函数返回之后,调用者就可以拿到函数当中赋予的值。

子进程的结束状态返回后存于status,底下有几个宏可判别结束情况

  • WIFEXITED(status)如果子进程正常结束则为非0值。
  • WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
  • WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真
  • WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
  • WIFSTOPPED(status)如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。
  • WSTOPSIG(status)取得引发子进程暂停的信号代码。

wait函数的四个特性:

1.输出型参数,与其对应的有:

  • 输入型参数→调用者给被调用函数传参;
  • 输入输出型参数;

2.int* status是一个指针类型占四个字节,但是实际中只使用到后两个字节,将这两个字节分为三部分:

  • 退出码 + coredump标志位 + 退出信号
  • 高位字节表示退出码;
  • 低位的一个字节又被分为两个部分,
  • 第一个比特位表示coredump标志位
  • 后七个比特位表示退出信号
    在这里插入图片描述

3.退出码:程序正常退出时用到

  • main函数的return返回时,return后的数字就是退出码
  • exit和_exit中的参数就是退出码

4.coredump标志位,退出信号是程序异常退出时用到:

  • coredump为0表示该进程在退出时没有内存镜像文件的产生(也就是说没有coredump产生),
  • 为1则有镜像文件产生,当前程序退出异常
  • 退出信号:进程异常退出时收到的几号信号
  • 进程正常退出时coredump标志位和退出信号被置为全0;
    退出码则是进程正常退出给的数值;
  • 异常退出coredump标志位和退出信号会有相应的数;
    也就是说程序正常退出只有退出码起作用,异常退出则要看标志位和退出信号

waitpid 函数
函数原型

#include <sys/types.h>
#include <sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);

返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果options指定了WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

pid:

  • pid<-1 等待进程组号为pid绝对值的任何子进程。
  • pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
  • pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
  • pid>0 等待进程号为pid的子进程,只要该子进程不结束,就会一直等待下去。

status:

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

options:
参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。
主要使用的有以下两个选项:

  • WNOHANG:如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
  • WUNTRACED:如果子进程进入暂停状态,则马上返回。

返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  1. 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
  2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在,当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

注意:

  • 第一层理解 :

对于 waitpid 函数就是 wait函数的增强版;
waitpid 函数 的使用方式 waitpid(-1,NULL,0) 等价 wait 函数的使用 wait(NULL) 两者这样使用一样的;

  • 第二层理解 :
  1. 对于status参数,其实是一个输出型参数,也就是父进程调用该waitpid时候,可以传入一个 地址给 status;待该waitpid执行结束返回时候,会得到该staus的值;
  2. status的值表示子进程的退出码的信息,也就是父进程为了得到子进程的退出信息,就是可以通过设置一个参数传入给status,获得子进程的信息;
  3. status的退出信息:也就是子进程退出的信息,而进程退出只有三种状态:正常退出执行代码结果正确,异常退出,正常退出了但是执行的结果不正确;异常退出的进程:本质是因为收到了某种信号,才会异常退出,而对于正常退出的进程,才有退出码而言说法;不管是信号还是退出码,都是子进程需要返回给父进程中stauts参数的;
  4. 其次这个 stauts,父进程获得子进程的status;不可以简简单单的认为 stauts就是一个整形int,我们要把它看为一个
    位图;对于32位的int类型来说:
    我们status是在每一个位上设置它的信息来使用的;高16位不使用,而低16位使用来表示具体信息;

在这里插入图片描述

  • 也就是说:对于子进程退出的状态信息:在8-16位的表示退出码的信息,低7位表示终止信号信息;而第8位单独一个表示core dump 状态,我们暂时不关心这个信息;

那么我们是如何获得该子进程的退出的信息的呢?

  • 我们可以通过
    (stauts >> 8 )& 0xFF;获得子进程的退出码;
    stauts & 0x7F; 获得子进程的终止信号;

8 僵尸进程与孤儿进程

会涉及到两个概念:父进程、子进程。
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法知道子进程到底何时结束。 当一个进程完成任务终止之后,它的父进程需要调用wait()或者waitpid()系统调用释放子进程。

僵尸进程:

  • 僵尸进程: 子进程结束以后, 父进程还在运行, 但是父进程不去释放进程控制块, 这个子进程就叫做僵尸进程。

  • 僵尸进程的产生:父进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

  • 系统为什么需要僵尸进程这种进程状态
    由于父进程创建子进程是异步的,双方不知道各自的运行状态,而父进程有的时候需要知道子进程退出时的一些信息,所以 linux提供了一种机制,通过让子进程退出时向父进程发送 SIGCHRD 信号来告知父进程,子进程已经退出了。同时,父进程通过调用 wait 和 waitpid 来获取子进程的退出信息。

  • 僵尸进程的危害:占用系统资源,造成内存泄漏

如何防止僵尸进程

  1. kill杀死元凶父进程(一般不用)
    严格的说,僵尸进程并不是问题的根源,罪魁祸首是产生大量僵死进程的父进程。因此,我们可以直接除掉元凶,通过kill发送SIGTERM或者SIGKILL信号。元凶死后,僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源。
    或者运行:kill -9 父进程的pid值

  2. 父进程用wait或waitpid去回收资源(方案不好)
    父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。

  3. 通过信号机制,在处理函数中调用wait,回收资源
    通过信号机制,子进程退出时向父进程发送SIGCHLD信号,父进程调用signal(SIGCHLD,sig_child)去处理SIGCHLD信号,在信号处理函数sig_child()中调用wait进行处理僵尸进程。什么时候得到子进程信号,什么时候进行信号处理,父进程可以继续干其他活,不用去阻塞等待。

孤儿进程:

  • 孤儿进程:父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由init进程(进程号PID = 1)回收。
  • 但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。

9 exec族函数(execl, execlp, execle, execv, execvp, execvpe)

exec族函数函数的作用:

我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序(即调用exec函数成功后,exec函数后面的原程序不会执行,而是执行exec调用的新程序)。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

exec族函数定义:

功能:在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe

函数原型

#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[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

返回值:

  • exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
  • 与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

参数说明:

  • path:可执行文件的路径名字
  • arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且 - arg必须以NULL结束
  • file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

  • l : 使用参数列表
  • p:使用文件名,并从PATH环境进行寻找可执行文件
  • v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
  • e:多了envp[ ]数组,使用新的环境变量代替调用进程的环境变量

1.带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。
以execl函数为例子来说明:

//文件execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

//函数原型:int execl(const char *path, const char *arg, ...);
//						可执行文件名字		可执行程序所带的参数,没有带路径且arg必须以NULL结束
//whereis指令查找程序的绝对路径

int main(void)
{
    printf("before execl\n");
    if(execl("./echoarg","echoarg","abcde",NULL) == -1)
    {
        printf("execl failed!\n");     
		perror("why"); //解析失败的原因
    }
    printf("after execl\n");//调用成功不执行下面代码
    return 0;
}
//文件echoarg.c
#include <stdio.h>

int main(int argc,char *argv[])
{
    int i = 0;
    for(i = 0; i < argc; i++)
    {
        printf("argv[%d]: %s\n",i,argv[i]); 
    }
    return 0;
}

实验结果:

ubuntu:~/test/exec_test$ ./execl
before execl****
argv[0]: echoarg
argv[1]: abc

实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径bin目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来(exec函数族的函数执行成功后不会返回)。

2.带p的一类exac函数,包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin

//文件execl_no_path.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
    printf("before execl****\n");
    if(execl("ps","ps","-l",NULL) == -1)
    {
        printf("execl failed!\n");
    }   
    printf("after execl*****\n");
    return 0;
}

实验结果:

ubuntu:~/test/exec_test$ gcc execl_no_path.c -o execl_no_path
ubuntu:~/test/exec_test$ ./execl_no_path 
before execl****
execl failed!
after execl*****

上面这个例子因为参数没有带路径,所以execl找不到可执行文件。
下面再看一个例子对比一下:

//文件execlp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execlp(const char *file, const char *arg, ...);
//函数原型:int execlp(const char *path, const char *arg, ...);
//上面的exaclp函数带p,所以能通过环境变量PATH查找到可执行文件ps
//echo ¥PATH查看当前环境变量。环境变量作用:系统找到这些路径底下的可执行程序。
//配置环境变量:1.pwd:显示当前路径	2.export PATH=¥PATH:...   (当前路径)
int main(void)
{
    printf("before execlp****\n");
    if(execlp("ps","ps","-l",NULL) == -1)
    {
        printf("execlp failed!\n");
    }
    printf("after execlp*****\n");
    return 0;
}

实验结果:

ubuntu:~/test/exec_test$ gcc execlp.c -o execlp
ubuntu:~/test/exec_test$ ./execlp
before execlp****
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 R  1048 35976 74920  0  80   0 -  2860 -      pts/4    00:00:00 ps
0 S  1048 74920 74916  0  80   0 -  7579 wait   pts/4    00:00:00 bash

从上面的实验结果可以看出,上面的exaclp函数带p,所以能通过环境变量PATH查找到可执行文件ps

3.带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL};
下面以execvp函数为例说明:

//文件execvp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execvp(const char *file, char *const argv[]);

int main(void)
{
    printf("before execlp****\n");
    char *argv[] = {"ps","-l",NULL};
    if(execvp("ps",argv) == -1) 
    {
        printf("execvp failed!\n");     
    }
    printf("after execlp*****\n");
    return 0;
}

实验结果:

ubuntu:~/test/exec_test$ gcc execvp.c -o execvp
ubuntu:~/test/exec_test$ ./execvp
before execlp****
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 R  1048 63491 74920  0  80   0 -  2860 -      pts/4    00:00:00 ps
0 S  1048 74920 74916  0  80   0 -  7579 wait   pts/4    00:00:00 bash

带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

4.带e的一类exac函数,包括execle、execvpe,可以传递一个指向环境字符串指针数组的指针。 参数例如char *env_init[] = {“AA=aa”,”BB=bb”,NULL}; 带e表示该函数取envp[]数组,而不使用当前环境。
下面以execle函数为例:

//文件execle.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execle(const char *path, const char *arg,..., char * const envp[]);

char *env_init[] = {"AA=aa","BB=bb",NULL};
int main(void)
{
    printf("before execle****\n");
        if(execle("./bin/echoenv","echoenv",NULL,env_init) == -1)
        {
                printf("execle failed!\n");
        }       
    printf("after execle*****\n");
    return 0;
}
//文件echoenv.c
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc , char *argv[])
{
    int i;
    char **ptr;
    for(ptr = environ;*ptr != 0; ptr++)
        printf("%s\n",*ptr);
    return 0;
}

实验结果:

ubuntu:~/test/exec_test$ gcc execle.c -o execle
ubuntu:~/test/exec_test$ ./execle
before execle****
AA=aa
BB=bb

我们先写一个显示全部环境表的程序,命名为echoenv.c,然后编译成可执行文件放到./bin目录下。然后再运行可执行文件execle,发现我们设置的环境变量确实有传进来。

在这里我们进一步了解一下main函数
main函数
在 C99 标准中,只有以下两种定义方式是正确的:

int main(void)
int main(int argc, char *argv[]) // char *argv[]可以写成char **argv
int main(int argc , char* argv[],char* envp[]);

若不需要从命令行中获取参数,就使用int main(void) ;
否则的话,就用int main( int argc, char *argv[] )。
main 函数的返回值类型必须是 int ,这样返回值才能传递给程序的调用者(如操作系统),等同于 exit(0),来判断函数的执行结果。

参数说明:

  • 第一个参数argc表示的是传入参数的个数 。

  • 第二个参数char* argv[],是字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。各成员含义如下:
    argv[0]:指向程序运行的全路径名。
    argv[1]:指向执行程序名后的第一个字符串 ,表示真正传入的第一个参数。
    argv[2]:指向执行程序名后的第二个字符串 ,表示传入的第二个参数。
    …… argv[n]:指向执行程序名后的第n个字符串 ,表示传入的第n个参数。
    规定:argv[argc]为NULL ,表示参数的结尾。

  • 第三个参数char* envp[],也是一个字符串数组,主要是保存这用户环境中的变量字符串,以NULL结束。envp[]的每一个元素都包含ENVVAR=value形式的字符串,其中ENVVAR为环境变量,value为其对应的值。
    envp一旦传入,它就只是单纯的字符串数组而已,不会随着程序动态设置发生改变。可以使用putenv函数实时修改环境变量,也能使用getenv实时查看环境变量,但是envp本身不会发生改变;平时使用到的比较少。

注意:main函数的参数char* argv[]和char* envp[]表示的是字符串数组,书写形式不止char* argv[]这一种,相应的argv[][]和 char** argv均可。

10 system函数和popen函数

system函数
函数原型

#include <stdlib.h>
int system(const char *command)

参数
command – 包含被请求变量名称的 C 字符串。

返回值
如果发生错误,则返回值为 -1,否则返回命令的状态。
(如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。如果 system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功)
system和exec不同的是,exec调用成功不会返回往下继续执行,而system调用成功会返回继续往下执行。

先看linux版system函数的源码:
代码:
#include
#include
#include
#include
int system(const char * cmdstring)
{
  pid_t pid;
  int status;

  if(cmdstring == NULL){
      
      return (1);
  }


  if((pid = fork())<0){

        status = -1;
  }
  else if(pid == 0){
    execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
    -exit(127); //子进程正常执行则不会执行此语句
    }
  else{
        while(waitpid(pid, &status, 0) < 0){
          if(errno != EINTER){
            status = -1;
            break;
          }
        }
    }
    return status;
}

也就是我们传递参数system,system中的execl接收并完成执行指令。
execl(“/bin/sh”, “sh”, “-c”, cmdstring, (char *)0);
这句话中sh -c是执行命令的前缀,和“./”一样,比如我们运行执行文件,第一种是./demo 还有一种是sh -c demo
例子如下:

#include <stdio.h>
#include <unistd.h>
//打印时间
//system 只需要传递一个命令参数,调用execl,执行起来应该是sh -c date
int main(int argc ,char **argv){
	
	
	if(system("date") == -1){
		printf("execl filed!\n");
		
		perror("becasue");
	}
	printf("system successful\n");
	return 0;
}

#include <stdio.h>
#include <string.h>
#include<stdlib.h>
 
int main ()
{
   char command[50];
 
   strcpy( command, "ls -l" );
   system(command);
 
   return(0);
}

实验结果:

drwxr-xr-x 2 apache apache 4096 Aug 22 07:25 hsperfdata_apache
drwxr-xr-x 2 railo railo 4096 Aug 21 18:48 hsperfdata_railo
rw------ 1 apache apache 8 Aug 21 18:48 mod_mono_dashboard_XXGLOBAL_1
rw------ 1 apache apache 8 Aug 21 18:48 mod_mono_dashboard_asp_2
srwx---- 1 apache apache 0 Aug 22 05:28 mod_mono_server_asp
rw------ 1 apache apache 0 Aug 22 05:28 mod_mono_server_asp_1280495620
srwx---- 1 apache apache 0 Aug 21 18:48 mod_mono_server_global

popen函数
函数原型

#include <stdio.h>
FILE *popen(const char *command,const char *type);

功能:popen打开的数据管道流,pclose关闭数据管道流。popen必须由pclose来关闭。popen()一般会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。
fclose函数原型:
#include <stdio.h>
int pclose(FILE *stream);
返回值:成功返回command终止状态,失败返回-1

参数说明:

  • command:shell 命令字符串的指针。
  • type:用来表示读(“r”)或者写(“w”)的,如果type是"r",则返回的文件指针是可读的,如果type是"w",则是可写的。不能同时为读和写。
  • 返回值:返回:若成功则为文件指针,若出错则为NULL。

比较system函数与popen函数:
system函数下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
 
int main(void)
{
        char ret[1024] = {0};
 
        system("ps");
 
        printf("ret=%s\n",ret);
 
        return 0;
}

运行结果:

CLC@Embed_Learn:~$ ./a1
  PID TTY          TIME CMD
 9796 pts/0    00:00:01 bash
10833 pts/0    00:00:00 a1
10834 pts/0    00:00:00 sh
10835 pts/0    00:00:00 ps
ret=

popen函数下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
 
int main(void)
{
        char ret[1024] = {0};
        FILE *fp;
 
        fp = popen("ps","r");
        int nread = fread(ret,1,1024,fp);
        system("ps");
 
        printf("read ret=%d byte ret=%s\n",nread,ret);
 
        return 0;
}

运行结果:

CLC@Embed_Learn:~$ ./a1
  PID TTY          TIME CMD
 9796 pts/0    00:00:01 bash
10878 pts/0    00:00:00 a1
10879 pts/0    00:00:00 sh <defunct>
10881 pts/0    00:00:00 sh
10882 pts/0    00:00:00 ps
read ret=138 byte ret=  PID TTY          TIME CMD
 9796 pts/0    00:00:01 bash
10878 pts/0    00:00:00 a1
10879 pts/0    00:00:00 sh
10880 pts/0    00:00:00 ps

对比system函数, popen函数可以获取运行的输出结果。

码字不易  求个三连

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大红烧肉

赠人玫瑰,手留余香。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值