1、Linux进程相关概念
1. 1程序和进程的区别
- 程序就像是一本食谱书,里面详细地写着怎么做一道菜(比如做饭)。
- 进程就像是根据食谱实际在厨房做菜的过程。每次你做饭,虽然使用同一本食谱,但每次的实际烹饪都是一个新的“进程”。
1.2查看系统中的进程
假如你想知道家里有哪些电器正在工作,你可能会查看每个房间的开关状态。在计算机中,想看哪些程序在运行,Windows 用户可以打开“任务管理器”,类似于检查每个房间的开关;而Mac或Linux用户可以用一个叫做“终端”的工具,输入特定的命令来查看。
1.3进程标识符(PID)
每个正在运行的进程都有一个特别的号码,就像每个正在上学的孩子都有一个独一无二的学号。这个号码帮助计算机识别和管理每个进程。
获取当前demo:
#include <stdio.h>
#include <unistd.h>
int main() {
// 获取当前进程的PID
pid_t pid = getpid();
// 打印PID
printf("当前进程的PID是: %d\n", pid);
return 0;
}
1.4父进程和子进程
- 父进程就像父母,可以“生”出孩子。在计算机中,某个进程可以创建其他进程,这个创建者就是“父进程”。
- 子进程就是被父进程创建出来的进程,就像孩子一样。比如,当你打开一个网页时(父进程),它可能会打开一个视频或音乐播放器(子进程)。
1.5C程序的存储空间分配
想象一下你的房间有几个不同的抽屉:
- 代码区像是放书的地方,存放你需要的食谱(程序代码)。
- 数据区是放长期使用物品的地方,比如放一年四季都要用的衣服(全局变量和静态变量)。
- 堆区就像一个大储藏室,你可以根据需要放进或取出东西(动态分配的内存)。
- 栈区像是放日常用品的抽屉,用完就清理,比如放你每天换下的衣服(函数调用的临时变量)。
1. 5.1正文段(Text Segment)
这部分是程序的"脑袋",存放着程序的指令或者代码。就像是你在做菜时跟随的食谱,正文段告诉计算机需要做什么,比如做加法、保存文件等操作。这部分是只读的,意味着你不能修改食谱上的内容,只能按照食谱去做。
1.5.2 初始化数据段(Initialized Data Segment)
这部分是程序的"储物柜",用来存放程序开始运行时就已经确定值的变量,如常量或者全局变量。比如,如果你在食谱上标注了"盐需用10克",这个信息就会存放在这里。这部分数据在程序开始之前就已经设定好了,程序运行时可以直接使用。
1.5.3非初始化数据段(Uninitialized Data Segment 或 BSS Segment)
这部分也是"储物柜",但用来存放还没有确定初始值的变量。想象你有一个空的瓶子用来装水,但你还没有决定要装多少水。程序开始时,这些变量通常会被自动初始化为零,直到程序运行过程中被赋予具体的值。
1.5.4 栈(Stack)
栈像是程序的"便签本",用于存放临时信息,比如函数的返回地址、局部变量等。每当程序调用一个函数时,就像是在便签本上写下需要暂时记住的信息,函数结束后,又将这些信息擦除。这样做可以帮助程序"记住"它在做什么,以及从哪里继续。
1.5.5.堆(Heap)
堆是程序的"自由存储区",可以在程序运行时动态地分配和释放内存。如果你在做菜时决定需要更多的盐,你可以从"盐罐"中取出所需的量。在程序中,如果你需要更多的内存空间来存储数据,你可以从堆中分配这些空间。
Windows
在Windows操作系统中,可以使用“任务管理器”来查看和管理进程:
- 通过快捷键打开: 按
Ctrl
+Shift
+Esc
或Ctrl
+Alt
+Delete
然后选择“任务管理器”。- 通过开始菜单打开: 点击开始菜单,搜索“任务管理器”,然后打开它。
在任务管理器的“进程”标签页中,你可以看到所有正在运行的应用程序和后台进程。
macOS
在macOS系统中,可以使用“活动监视器”来查看进程:
- 打开“启动台”,在其他文件夹中找到并启动“活动监视器”。
- 或者,你可以使用 Spotlight 搜索(按
Cmd
+Space
),输入“活动监视器”,然后打开它。在活动监视器中,你可以看到系统中所有的进程及其状态、内存占用、CPU 使用率等信息。
Linux
在Linux系统中,通常通过终端(Terminal)使用命令来查看进程:
ps
命令:ps
是“process status”的缩写,可以快速查看当前终端下的进程。常用的命令为ps aux
来查看系统中所有的进程,例如:ps -aux|grep init 。top
命令: 这个命令提供了一个实时的进程监视,显示系统中进程的详细列表,以及关于CPU和内存使用的统计信息。htop
命令(需要先安装):htop
是top
的一个增强版,提供更多信息,界面也更为友好。
2、创建fork
fork()
函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork()
有三种可能的返回值,它们在父进程和子进程中的含义不同:
- -1: 如果进程创建失败,
fork()
返回 -1。失败的原因通常与系统资源限制(如进程数限制)有关,详细的错误信息会被存储在全局变量errno
中。- 0:
fork()
在新创建的子进程中返回 0。这允许新的进程知道它是通过fork()
创建的子进程。- 大于0:
fork()
在父进程中返回新创建的子进程的进程ID(PID)。这允许父进程知道它的子进程的PID,用于后续的进程管理操作,如监控子进程的状态或是等待子进程结束。
2.1创建进程fork以及函数的使用
#include <stdio.h> // 引入标准输入输出库,用于printf函数
#include <sys/types.h> // 引入类型库,定义了一些数据类型,比如pid_t
#include <unistd.h> // 引入POSIX操作系统API,包括getpid()和fork()函数
int main()
{
pid_t pid; // 定义一个pid_t类型的变量,用来存储进程ID
pid = getpid(); // 调用getpid()函数获取当前进程的ID,并存储在变量pid中
fork(); // 调用fork()函数创建一个新的进程,这是当前进程的复制
printf("my pid is %d\n",pid); // 打印变量pid的值,即父进程的进程ID
return 0; // 程序正常结束,返回0
}
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含系统数据类型定义
#include <unistd.h> // 包含Unix标准函数定义
int main()
{
pid_t pid; // 定义一个变量pid,类型为pid_t,用于存储进程ID
pid = getpid(); // 获取当前进程的PID,并将其赋值给pid变量
fork(); // 创建一个子进程
printf("my pid is %d, current pro id:%d\n", pid, getpid()); // 打印原始pid变量值和当前进程的PID
return 0;
}
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含系统数据类型定义
#include <unistd.h> // 包含Unix标准函数定义
int main()
{
pid_t pid; // 定义一个变量pid,类型为pid_t,用于存储进程ID
pid_t pid2; // 定义另一个变量pid2,类型为pid_t,用于存储进程ID
pid = getpid(); // 获取当前进程的PID,并将其赋值给pid变量
printf("before fork: pid = %d\n", pid); // 在fork之前打印pid变量的值
fork(); // 创建一个子进程
pid2 = getpid(); // 获取当前进程的PID,并将其赋值给pid2变量
printf("after fork: pid = %d\n", pid2); // 打印fork之后的pid2变量值
if (pid == pid2) // 判断当前进程是否为父进程
{
printf("this is father print\n"); // 如果是父进程,打印相应信息
}
else
{
printf("this is child print,child pid = %d\n", getpid()); // 如果是子进程,打印相应信息和子进程的PID
}
return 0;
}
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含系统数据类型定义
#include <unistd.h> // 包含Unix标准函数定义
int main()
{
pid_t pid; // 定义一个变量pid,类型为pid_t,用于存储进程ID
printf("father: id=%d\n", getpid()); // 打印当前进程的PID,作为父进程
pid = fork(); // 创建一个子进程
if (pid > 0) // 如果返回值大于0,则表示在父进程中
{
printf("this is father print, pid = %d\n", getpid()); // 打印父进程的PID
}
else if (pid == 0) // 如果返回值等于0,则表示在子进程中
{
printf("this is child print,child pid = %d\n", getpid()); // 打印子进程的PID
}
return 0;
}
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含系统数据类型定义
#include <unistd.h> // 包含Unix标准函数定义
int main()
{
pid_t pid; // 定义一个变量pid,类型为pid_t,用于存储进程ID
pid_t pid2; // 定义另一个变量pid2,类型为pid_t,用于存储进程ID
pid_t retpid; // 定义一个变量retpid,类型为pid_t,用于存储fork()的返回值
pid = getpid(); // 获取当前进程的PID,并将其赋值给pid变量
printf("before fork: pid = %d\n", pid); // 在fork之前打印pid变量的值
retpid = fork(); // 创建一个子进程,并将返回值赋值给retpid变量
pid2 = getpid(); // 获取当前进程的PID,并将其赋值给pid2变量
printf("after fork: pid = %d\n", pid2); // 打印fork之后的pid2变量值
if (pid == pid2) // 判断当前进程是否为父进程
{
printf("this is father print: iretpid = %d\n", retpid); // 如果是父进程,打印相应信息和fork的返回值
}
else
{
printf("this is child print,retpid=%d,child pid = %d\n", retpid, getpid()); // 如果是子进程,打印相应信息、fork的返回值和子进程的PID
}
return 0;
}
2.2通过调用fork()
函数创建一个子进程,并通过比较进程ID来区分父进程和子进程,分别打印不同的信息。
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含系统数据类型定义
#include <unistd.h> // 包含Unix标准函数定义
int main()
{
pid_t pid; // 定义一个变量pid,类型为pid_t,用于存储进程ID
pid_t pid2; // 定义另一个变量pid2,类型为pid_t,用于存储进程ID
pid_t retpid; // 定义一个变量retpid,类型为pid_t,用于存储fork()的返回值
pid = getpid(); // 获取当前进程的PID,并将其赋值给pid变量
printf("before fork: pid = %d\n", pid); // 在fork之前打印pid变量的值
retpid = fork(); // 创建一个子进程,并将返回值赋值给retpid变量
pid2 = getpid(); // 获取当前进程的PID,并将其赋值给pid2变量
printf("after fork: pid = %d\n", pid2); // 打印fork之后的pid2变量值
if (pid == pid2) // 判断当前进程是否为父进程
{
printf("this is father print: iretpid = %d\n", retpid); // 如果是父进程,打印相应信息和fork的返回值
}
else
{
printf("this is child print,,retpid=%d,child pid = %d\n", retpid, getpid()); // 如果是子进程,打印相应信息、fork的返回值和子进程的PID
}
return 0;
}
2.3进程之间发生了什么事
定义了一个
pid_t
类型的变量pid
来接收fork()
调用的结果,这个函数用于创建一个新的子进程。根据fork()
返回的值,可以区分父进程和子进程:父进程中fork()
返回子进程的PID,而子进程中返回0。在子进程中,程序修改了变量data
的值。最后,无论是父进程还是子进程都会打印变量data
的值。这样的设计使得在父子进程中
data
的值有所不同,因为它们实际上在不同的内存空间中分别持有data
变量的副本。父进程中data
保持为10,而在子进程中被修改为110。
#include <stdio.h> // 引入标准输入输出头文件,提供打印等功能
#include <sys/types.h> // 引入数据类型头文件,包括pid_t
#include <unistd.h> // 引入POSIX操作系统API,提供fork()函数
int main() // 主函数,程序执行的入口
{
pid_t pid; // 定义进程ID变量pid,用于存储fork函数的返回值
int data = 10; // 定义一个整数变量data,并初始化为10
printf("father: id=%d\n", getpid()); // 打印父进程的进程ID
pid = fork(); // 调用fork函数创建新的子进程,返回值存储在pid中
if (pid > 0) // 如果pid大于0,说明当前在父进程中
{
printf("this is father print, pid = %d\n", getpid()); // 打印父进程ID
}
else if (pid == 0) { // 如果pid等于0,说明当前在子进程中
printf("this is child print, child pid = %d\n", getpid()); // 打印子进程ID
data = data + 100; // 子进程中修改data变量的值
}
printf("data=%d\n", data); // 打印当前进程中data的值
return 0; // 程序正常退出
}
创建一个子进程的一般目的k创建
2.4 fork创建一个子进程的一般目的
2.5fork总结
C 语言中编写的无限循环程序,其目的是通过用户输入决定是否创建一个新的子进程,该子进程会周期性地执行一些任务(比如模拟网络请求)
主进程持续等待用户输入。当输入为1时,它将创建一个子进程。该子进程会进入自己的无限循环中,每隔三秒打印一次消息,并通过
sleep(3)
模拟延时。父进程在每次创建子进程后不会进行任何操作,而是继续在外层循环等待更多输入。如果输入不是1,主进程会输出等待的消息并继续等待下一个输入。这种设计允许程序根据用户的输入不断创建新的子进程。
#include <stdio.h> // 引入标凲输入输出头文件,提供打印和读取功能
#include <sys/types.h> // 引入数据类型头文件,包括pid_t
#include <unistd.h> // 引入POSIX操作系统API,提供fork()和sleep()函数
int main() // 主函数,程序执行的入口
{
pid_t pid; // 定义进程ID变量pid,用于存储fork函数的返回值
int data = 10; // 定义一个整数变量data,并初始化为10
while(1){ // 无限循环
printf("please input a data\n"); // 提示用户输入数据
scanf("%d", &data); // 读取用户输入的整数,并存储在变量data中
if (data == 1) { // 如果用户输入的是1
pid = fork(); // 调用fork函数创建新的子进程,返回值存储在pid中
if (pid > 0) // 如果pid大于0,说明当前在父进程中
{
// 父进程不做任何操作,继续循环等待新的输入
}
else if (pid == 0) { // 如果pid等于0,说明当前在子进程中
while (1) { // 子进程中进入另一个无限循环
printf("do net request, pid=%d\n", getpid()); // 打印执行网络请求的消息和进程ID
sleep(3); // 暂停3秒,模拟网络请求的耗时操作
}
}
}
else { // 如果用户输入的不是1
printf("wait, do nothing\n"); // 提示等待,不执行任何操作
}
}
return 0; // 实际上,程序设计为永远不会到达这里
}
3、vfork创建进程
3.1vfork函数 也可以创建进程,与fork有什么区别
3.1.1关键区别一:
3.1.2vfork 直接使用父进程存储空间,不拷贝。
3.1.3关键区别二:
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
这个示例演示使用 fork()
函数创建子进程,fork()
会复制父进程的存储空间。
#include <stdio.h>
#include <unistd.h> // 提供fork()函数
int main() {
pid_t pid;
int data = 10; // 初始化数据变量
pid = fork(); // 创建子进程,父子进程拥有各自独立的数据拷贝
if (pid == 0) { // 子进程
printf("Child process with fork, data=%d\n", data);
data += 5; // 子进程改变数据
printf("Child changed data to %d\n", data);
_exit(0); // 子进程退出
} else { // 父进程
sleep(1); // 延迟父进程,确保子进程先运行
printf("Parent process with fork, data=%d\n", data);
}
return 0;
}
在这个
fork()
示例中,父子进程各自拥有独立的内存拷贝,所以子进程对data
的修改不会影响父进程中的data
值。
这个示例演示使用 vfork()
函数创建子进程,vfork()
不会复制父进程的存储空间,保证子进程先运行。
#include <stdio.h>
#include <unistd.h> // 提供vfork()函数
#include <stdlib.h> // 提供exit()函数
int main() {
pidome om, int data = 10; // 初始化数据变量
pid = vfork(); // 创建子进程,子进程使用父进程的存储空间
if (pid == 0) { // 子进程
printf("Child process with vfork, data=%d\n", data);
data += 5; // 子进程改变数据
printf("Child changed data to %d\n", data);
exit(0); // 子进程退出,释放控制权给父进程
} else { // 父进程
printf("Parent process with vfork, data=%d\n", data);
}
return 0;
}
在
vfork()
示例中,由于子进程使用父进程的存储空间,子进程对data
的修改会直接影响到父进程的data
值。此外,vfork()
保证子进程先运行直到它调用exit()
或exec()
,父进程才会继续执行。
3.2进程的退出
正常退出方式
Main函数调用return: 当
main()
函数执行完毕并返回一个值时,该返回值会被传递给操作系统,表示程序的退出状态。进程调用
exit()
: 这是标准C库中的一个函数,用来结束程序的执行,并将控制权返回给操作系统。exit()
会先执行注册的退出函数(通过atexit()
注册的函数),关闭所有标准I/O流等清理工作。原型:void exit(int status);
进程调用
_exit()
或_Exit()
: 这些是系统调用,用于立即终止程序,不执行exit()
中的任何清理工作如关闭文件描述符、执行atexit()
注册的函数等。原型:void _exit(int status); 原型:void _Exit(int status);
进程最后一个线程返回: 如果进程中的最后一个线程执行完其启动例程,则该进程会结束。
最后一个线程调用
pthread_exit
: 结束调用线程而不是整个进程。
异常退出方式
- 调用
abort()
: 用于异常结束程序,通常是因为遇到了错误或不可恢复的问题。- 当进程收到某些信号时,如CTRL+C: 这会导致进程接收到终止信号,如 SIGINT,通常导致进程异常退出。
- 最后一个线程对取消请求做出响应: 线程库中的线程可能会对取消请求做出响应,从而结束线程和相关进程。
如何使用 exit()
函数来正常结束程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // 包含 _exit() 和 _Exit()
void cleanup(void) {
printf("执行 atexit() 注册的清理函数。\n");
}
int main() {
atexit(cleanup); // 注册 cleanup 函数,它会在调用 exit() 时执行
printf("程序开始。\n");
int condition = 1; // 条件变量,用于控制循环
int method = 3; // 设置退出方法:1 for exit(), 2 for _exit(), 3 for _Exit()
while (condition) {
if (method == 1) {
printf("使用 exit() 正常退出。\n");
exit(0); // 调用 exit(),执行注册的清理函数
} else if (method == 2) {
printf("使用 _exit() 系统调用退出。\n");
_exit(0); // 调用 _exit(),立即终止程序,不执行任何清理操作
} else if (method == 3) {
printf("使用 _Exit() C11标准函数退出。\n");
_Exit(0); // 调用 _Exit(),与 _exit() 行为相同
}
break; // 在执行了任一退出操作后退出循环
}
printf("这行代码不会被执行。\n");
return 0; // 这行代码也不会执行
}
3.3父进程等待子进程退出
3.3.1僵尸进程(子进程退出状态不被父进程收集)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
// 主函数
int main()
{
pid_t pid; // 定义进程ID变量
int cnt = 0; // 初始化计数器变量
pid = fork(); // 创建子进程
// 父进程部分
if(pid > 0){
while(1){
printf("这是父进程打印: pid = %d \n",getpid()); // 打印父进程的进程ID
printf("cnt = %d\n",cnt); // 打印计数器值
sleep(1); // 每秒休眠一次
}
}
// 子进程部分
else if(pid == 0){
while(1){
printf("这是子进程打印: pid = %d \n",getpid()); // 打印子进程的进程ID
cnt++; // 计数器递增
if(cnt == 3){ // 当计数器等于3时
exit(0); // 退出子进程
}
sleep(1); // 每秒休眠一次
}
}
return 0; // 返回0,结束程序
}
3.3.2子进程退出状态被父进程收集,调用wait
在等待的过程中:
如果其所有子进程都还在运行,则阻塞。
如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
如果它没有任何子进程,则立即出错返回。
status参数:是一个整型数指针
非空:
子进程退出状态存放在它指向的地址中
空:
不关心退出状态
#include <stdio.h> // 包含标准输入输出库
#include <sys/types.h> // 包含基本系统数据类型
#include <unistd.h> // 包含Unix标准函数定义
#include <stdlib.h> // 包含标准库函数
int main()
{
pid_t pid; // 定义进程ID类型变量pid
int cnt = 0; // 定义并初始化变量cnt为0
int status = 10; // 定义并初始化变量status为10
pid = fork(); // 创建一个子进程
if (pid > 0) // 如果pid大于0,说明在父进程中
{
wait(&status); // 等待子进程结束,并获取子进程的退出状态
printf("child quit, child status = %d\n", WEXITSTATUS(status)); // 输出子进程的退出状态
while (1) { // 无限循环
printf("cnt=%d\n", cnt); // 输出当前cnt的值
printf("this is father print, pid = %d\n", getpid()); // 输出父进程的进程ID
sleep(1); // 休眠1秒
}
}
else if (pid == 0) // 如果pid等于0,说明在子进程中
{
while (1) { // 无限循环
printf("this is child print, pid = %d\n", getpid()); // 输出子进程的进程ID
sleep(1); // 休眠1秒
cnt++; // 变量cnt自增1
if (cnt == 5) { // 如果cnt等于5
exit(3); // 子进程退出,返回状态码3
}
}
}
return 0; // 主程序返回0
}
3.3.3waitpid 使调用者阻塞,waitpid有一个选项,可以使调用者不阻塞。
#include <unistd.h> // 包含Unix标准函数定义
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库函数
int main()
{
pid_t pid; // 定义进程ID类型变量pid
int cnt = 0; // 定义并初始化变量cnt为0
int status = 10; // 定义并初始化变量status为10
pid = fork(); // 创建一个子进程
if (pid > 0) { // 如果pid大于0,说明在父进程中
// wait(&status); // 等待子进程结束,并获取子进程的退出状态(已注释掉)
waitpid(pid, &status, WNOHANG); // 非阻塞地等待特定子进程结束,并获取其退出状态
printf("child quit, child status = %d\n", WEXITSTATUS(status)); // 输出子进程的退出状态
while (1) { // 无限循环
printf("cnt = %d\n", cnt); // 输出当前cnt的值
printf("this is father print: pid = %d \n", getpid()); // 输出父进程的进程ID
sleep(1); // 休眠1秒
}
}
else if (pid == 0) { // 如果pid等于0,说明在子进程中
while (1) { // 无限循环
printf("this is child print: pid = %d \n", getpid()); // 输出子进程的进程ID
cnt++; // 变量cnt自增1
if (cnt == 5) { // 如果cnt等于5
exit(3); // 子进程退出,返回状态码3
}
sleep(1); // 休眠1秒
}
}
return 0; // 主程序返回0
}
3.3.4孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时的子进程叫做孤儿进程Linux避免系统存在过多的孤儿进程,init进程(系统的一个初始化进程,它的pid号为1)收留孤儿进程,变成孤儿进程的父进程
#include <unistd.h> // 包含Unix标准函数定义
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库函数
int main()
{
pid_t pid; // 定义进程ID类型变量pid
int cnt = 0; // 定义并初始化变量cnt为0
int status = 10; // 定义并初始化变量status为10
pid = fork(); // 创建一个子进程
if (pid > 0) { // 如果pid大于0,说明在父进程中
printf("this is father print: pid = %d \n", getpid()); // 输出父进程的进程ID
}
else if (pid == 0) { // 如果pid等于0,说明在子进程中
while (1) { // 无限循环
printf("this is child print: pid = %d, my father pid = %d \n", getpid(), getppid()); // 输出子进程的进程ID和父进程的进程ID
cnt++; // 变量cnt自增1
if (cnt == 5) { // 如果cnt等于5
exit(3); // 子进程退出,返回状态码3
}
sleep(1); // 休眠1秒
}
}
return 0; // 主程序返回0
}
4、exec族函数
4.1为什么要用exec族函数,有什么作用
exec族函数的作用
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变
exec族函数功能
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
函数族
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
函数原型:
#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 execvpe(const char *file, char *const argv[],char *const envp[]);
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:
path:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
4.2以execl函数为例子来编写代码说明:
带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。
//文件execl.c
#include <stdio.h> // 引入标准输入输出库,用于printf等函数
#include <stdlib.h> // 引入标准库头文件,用于各种类型的常规操作,虽然在这个程序中未直接使用
#include <unistd.h> // 引入POSIX操作系统API的头文件,包含各种UNIX系统服务的函数声明,如execl
// 函数原型:int execl(const char *path, const char *arg, ...);
int main(void) // 主函数入口,程序从这里开始执行
{
printf("before execl\n"); // 打印消息,表明execl函数调用前的状态
// 调用execl函数尝试执行当前目录下名为"echoarg"的程序,程序名后的参数为"abc",NULL表示参数列表的结束
if(execl("./echoarg", "echoarg", "abc", NULL) == -1)
{
printf("execl failed!\n"); // 如果execl调用失败(返回值为-1),打印错误信息
perror("why"); // 使用perror打印execl失败的具体错误原因,"why"是错误消息前的前缀
}
printf("after execl\n"); // 打印消息,表明execl调用后的状态,通常这行代码不会被执行,因为execl替换了当前进程
return 0; // 主函数返回0,正常退出程序
}
//文件echoarg.c
#include <stdio.h> // 引入标准输入输出库头文件,用于使用printf函数
int main(int argc, char *argv[]) // 主函数入口,带有两个参数:argc(参数数量)和argv(参数字符串数组)
{
int i = 0; // 定义整数i用于循环计数
for (i = 0; i < argc; i++) // for循环,从0开始,持续到i小于argc(即遍历所有命令行参数)
{
printf("argv[%d]: %s\n", i, argv[i]); // 使用printf打印每个参数的索引和内容
}
return 0; // 返回0,正常退出程序
}
实验结果:
ubuntu:~/test/exec_test$ ./execl
before execl****
argv[0]: echoarg
argv[1]: abc
实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
4.3活用execl族函数来查找系统时间
首先用命令 whereis date ,找到系统时间 date 的绝对路径:
#include <stdio.h> // 引入标准输入输出库头文件,用于使用 printf 等函数。
#include <stdlib.h> // 引入标准库头文件,提供通用功能的函数,虽然此程序未直接使用。
#include <unistd.h> // 引入POSIX操作系统API的头文件,包含各种UNIX系统服务的函数声明,如 execl。
// 函数原型:int execl(const char *path, const char *arg, ...);
int main(void) // 程序的主函数入口
{
printf("this pro get system data\n"); // 打印信息,说明程序的功能是获取系统日期。
// 调用 execl 函数尝试执行系统的 date 命令,第一个参数是命令的完整路径,第二个参数是命令名,后跟NULL表示参数结束。
if (execl("/bin/date", "date", NULL) == -1)
{
printf("execl failed!\n"); // 如果 execl 调用失败(返回值为-1),打印失败信息。
perror("why"); // 使用 perror 打印 execl 调用失败的具体错误原因,"why" 是错误消息前的前缀。
}
printf("after execl\n"); // 打印消息,表明 execl 调用后的状态,通常这行代码不会被执行,因为 execl 替换了当前进程。
return 0; // 主函数返回0,正常退出程序。
}
4.4 execlp函数
#include <stdio.h> // 引入标准输入输出库头文件,用于使用 printf 等函数。
#include <unistd.h> // 引入POSIX操作系统API的头文件,包含各种UNIX系统服务的函数声明,如 execlp。
int main(void) // 程序的主函数入口
{
printf("before execl\n"); // 打印信息,表明即将调用 execlp 函数。
// 调用 execlp 函数尝试执行系统的 ps 命令,第一个参数是命令名,后跟 NULL 表示参数结束。
if (execlp("ps", "ps", NULL) == -1)
{
printf("execl failed\n"); // 如果 execlp 调用失败(返回值为-1),打印失败信息。
perror("why"); // 使用 perror 打印 execlp 调用失败的具体错误原因,"why" 是错误消息前的前缀。
}
printf("after execl\n"); // 打印消息,表明 execlp 调用后的状态,通常这行代码不会被执行,因为 execlp 替换了当前进程。
return 0; // 主函数返回0,正常退出程序。
}
4.5execvp
#include <stdio.h> // 引入标准输入输出库头文件,用于使用 printf 等函数。
#include <unistd.h> // 引入POSIX操作系统API的头文件,包含各种UNIX系统服务的函数声明,如 execvp。
int main(void) // 程序的主函数入口
{
printf("before execl\n"); // 打印信息,表明即将调用 execvp 函数。
char *argv[] = {"ps", NULL, NULL}; // 定义一个字符串数组用于存储命令行参数,其中"ps"是要执行的命令,后跟两个NULL。
// 注意这里第二个NULL是多余的,一个就足够了。
if (execvp("ps", argv) == -1) // 调用 execvp 执行命令"ps",argv为命令行参数数组。如果调用失败返回-1。
{
printf("execl failed\n"); // 如果 execvp 调用失败,打印失败信息。
perror("why"); // 使用 perror 打印 execvp 调用失败的具体错误原因,"why" 是错误消息前的前缀。
}
printf("after execl\n"); // 打印消息,表明 execvp 调用后的状态,通常这行代码不会被执行,因为 execvp 替换了当前进程。
return 0; // 主函数返回0,正常退出程序。
}
找出当前路径: 使用命令
pwd
可以显示当前工作目录的绝对路径。例如:pwd
假设输出是
/home/username/mydir
,这就是你当前的工作目录。查看当前的 PATH 环境变量: 虽然你提到这一步可以省略,但为了完整演示,可以用
echo $PATH
查看当前的PATH
环境变量。echo $PATH
将当前路径添加到 PATH 环境变量: 通过以下命令,你可以将步骤1中得到的路径添加到
PATH
环境变量中。确保替换/home/username/mydir
为你实际的路径。export PATH=$PATH:/home/username/mydir
这样做之后,
/home/username/mydir
目录中的所有可执行文件都可以直接通过文件名来运行,而不需要指定路径。这一更改只在当前终端会话中有效,一旦你关闭终端或重新登录,这个更改会失效。如果你想让这个更改永久有效,你需要将这条
export
命令添加到你的~/.bashrc
或~/.profile
文件中。可以使用文本编辑器,例如nano
或vim
,来编辑这些文件:nano ~/.bashrc
然后在文件的末尾添加上面的
export
命令。保存并退出编辑器后,为了让更改生效,需要重新加载配置文件:source ~/.bashrc
4.6Linux下exec配合fork使用
实现功能,当父进程检测到输入为1的时候,创建子进程把配置文件的字段值修改掉。
被修改的字段的配置文件config.txt
//config.txt
SPEED=5
LENG=9
SCORE=90
LEVEL=95
修改字段的文件 changData.c
#include <sys/types.h> // 引入类型定义,用于后续的系统调用
#include <sys/stat.h> // 引入与文件状态相关的定义
#include <fcntl.h> // 引入文件控制定义,例如 O_RDWR
#include <stdio.h> // 引入标准输入输出库
#include <unistd.h> // 引入POSIX操作系统API
#include <string.h> // 引入字符串处理函数
#include <stdlib.h> // 引入标准库头文件,用于动态内存管理等
int main(int argc, char **argv) // 主函数,接受命令行参数
{
int fdSrc; // 文件描述符
char *readBuf = NULL; // 读缓冲区指针
if (argc != 2) { // 检查参数数量是否正确
printf("parameters error\n"); // 参数错误提示
exit(-1); // 非正常退出程序
}
fdSrc = open(argv[1], O_RDWR); // 打开文件,参数 argv[1] 是文件名,O_RDWR 表示读写方式打开
int size = lseek(fdSrc, 0, SEEK_END); // 移动文件指针到文件末尾,获取文件大小
lseek(fdSrc, 0, SEEK_SET); // 重新定位文件指针到文件开头
readBuf = (char *)malloc(sizeof(char) * size + 8); // 分配足够的内存以存储文件内容和额外空间
int n_read = read(fdSrc, readBuf, size); // 从文件中读取数据到缓冲区
char *p = strstr(readBuf, "LENG="); // 在缓冲区中查找字符串 "LENG="
if (p == NULL) { // 如果没找到
printf("not found\n");
exit(-1); // 非正常退出程序
}
p = p + strlen("LENG="); // 将指针移动到 "LENG=" 后的位置
*p = '5'; // 修改该位置的字符为 '5'
lseek(fdSrc, 0, SEEK_SET); // 重新定位文件指针到文件开头
int n_write = write(fdSrc, readBuf, strlen(readBuf)); // 将修改后的缓冲区内容写回文件
close(fdSrc); // 关闭文件
return 0; // 正常退出程序
}
将修改字段的文件changData.c ,( gcc changData.c -o changData ) ,生成可执行文件 changData
由下面 execl( ) 函数配合 fork( ) 函数使用的代码,让 exexl( ) 函数调用可执行文件 changData,来修改配置文件
//demo.c
#include <stdio.h> // 引入标准输入输出库头文件,用于 printf 和 scanf 等函数。
#include <sys/types.h> // 引入数据类型,通常用于系统调用。
#include <unistd.h> // 引入POSIX操作系统API,包括 fork 和 execl 函数。
#include <sys/stat.h> // 引入文件状态相关定义,此程序未使用。
#include <fcntl.h> // 引入文件控制定义,此程序未使用。
#include <string.h> // 引入字符串处理函数,此程序未使用。
#include <stdlib.h> // 引入标准库,此程序未使用。
int main() // 主函数入口
{
pid_t pid; // 用于存储 fork 函数返回的进程ID
int data = 10; // 初始化数据变量,用于存储用户输入
while(1){ // 无限循环
printf("please input your data:\n"); // 提示用户输入数据
scanf("%d", &data); // 读取用户输入的整数
if(data == 1){ // 如果输入的数据为1
pid = fork(); // 创建子进程
if(pid > 0){ // 如果是父进程
wait(NULL); // 父进程等待子进程结束
}
if(pid == 0){ // 如果是子进程
while(1){ // 子进程无限循环
// 在子进程中调用 execl 来执行另一个程序 changData
execl("./changData", "changData", "TEST.config", NULL);
}
}
}
else{ // 如果输入的数据不是1
printf("wait, do nothing!\n"); // 打印等待消息,不执行任何操作
}
}
return 0; // 程序不应该到达这里,因为上面的循环是无限的
}
配置文件被修改后:将 LENG=9 改成了 LENG=5
//config.txt
SPEED=5
LENG=5
SCORE=90
LEVEL=95
5、system函数
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
ststem()函数返回值
成功,则返回进程的状态值;
当sh不能执行时,返回127;
失败返回-1;
system()函数源码
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL)
{
return (1); //如果cmdstring为空,返回非零值,一般为1
}
if((pid = fork())<0)
{
status = -1; //fork失败,返回-1
}
else if(pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); /* exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的
进程就不存在啦8*/
}
else //父进程
{
while(waitpid(pid, &status, 0) < 0)
{
if(errno != EINTR)
{
status = -1; //如果waitpid被信号中断,则返回-1
break;
}
}
}
return status; //如果waitpid成功,则返回子进程的返回状态
}
system()函数小应用代码demo
实现小功能,执行 vim 中的 ps - l 命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int system(const char *command);
int main(void)
{
printf("this pro get system date:\n");
if(system("ps -l") == -1)
{
printf("system failed!\n");
perror("why");
}
printf("after system!!!\n");
return 0;
}
通过运行结果可以看出:system( ) 函数调用完之后,代码还会往下走。 而exec族函数则不会往下走。
system( ) 函数的参数书写的规律是,可执行文件怎么执行,就怎么写:比如 system(“./a.out aa bb”);
popen函数
SYNOPSIS
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函数说明
popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。这个管道必须由pclose()函数关闭,而不是fclose()函数。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。
type参数只能是读或者写中的一种,得到的返回值(标准I/O流)也具有和type相应的只读或只写类型。如果type是"r"则文件指针连接到command的标准输出;如果type是"w"则文件指针连接到command的标准输入。
command参数是一个指向以NULL结束的shell命令字符串的指针。这行命令将被传到bin/sh并使用-c标志,shell将执行这个命令。
popen()的返回值是个标准I/O流,必须由pclose来终止。前面提到这个流是单向的(只能用于读或写)。向这个流写内容相当于写入该命令的标准输入,命令的标准输出和调用popen()的进程相同;与之相反的,从流中读数据相当于读取命令的标准输出,命令的标准输入和调用popen()的进程相同。
返回值
如果调用fork()或pipe()失败,或者不能分配内存将返回NULL,否则返回标准I/O流。popen()没有为内存分配失败设置errno值。如果调用fork()或pipe()时出现错误,errno被设为相应的错误类型。如果type参数不合法,errno将返回EINVAL。
比system()函数在应用中的好处:可以获取运行的结果
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//FILE *popen(const char *command, const char *type);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
int main(void)
{
FILE *fp;
char ret[1024]={0};
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte,ret = %s \n",nread,ret);
return 0;
}