Linux进程

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操作系统中,可以使用“任务管理器”来查看和管理进程:

  1. 通过快捷键打开:Ctrl + Shift + EscCtrl + Alt + Delete 然后选择“任务管理器”。
  2. 通过开始菜单打开: 点击开始菜单,搜索“任务管理器”,然后打开它。

在任务管理器的“进程”标签页中,你可以看到所有正在运行的应用程序和后台进程。

macOS

在macOS系统中,可以使用“活动监视器”来查看进程:

  1. 打开“启动台”,在其他文件夹中找到并启动“活动监视器”。
  2. 或者,你可以使用 Spotlight 搜索(按 Cmd + Space),输入“活动监视器”,然后打开它。

在活动监视器中,你可以看到系统中所有的进程及其状态、内存占用、CPU 使用率等信息。

Linux

在Linux系统中,通常通过终端(Terminal)使用命令来查看进程:

  • ps 命令: ps 是“process status”的缩写,可以快速查看当前终端下的进程。常用的命令为 ps aux 来查看系统中所有的进程,例如:ps -aux|grep init 。
  • top 命令: 这个命令提供了一个实时的进程监视,显示系统中进程的详细列表,以及关于CPU和内存使用的统计信息。
  • htop 命令(需要先安装): htoptop 的一个增强版,提供更多信息,界面也更为友好。

 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进程的退出

正常退出方式
  1. Main函数调用return:main() 函数执行完毕并返回一个值时,该返回值会被传递给操作系统,表示程序的退出状态。

  2. 进程调用 exit() 这是标准C库中的一个函数,用来结束程序的执行,并将控制权返回给操作系统。exit() 会先执行注册的退出函数(通过 atexit() 注册的函数),关闭所有标准I/O流等清理工作。

  3. 原型:void exit(int status);

    进程调用 _exit()_Exit() 这些是系统调用,用于立即终止程序,不执行 exit() 中的任何清理工作如关闭文件描述符、执行atexit()注册的函数等。

  4. 原型:void _exit(int status);
    原型:void _Exit(int status);
  5. 进程最后一个线程返回: 如果进程中的最后一个线程执行完其启动例程,则该进程会结束。

  6. 最后一个线程调用 pthread_exit 结束调用线程而不是整个进程。

异常退出方式
  1. 调用 abort() 用于异常结束程序,通常是因为遇到了错误或不可恢复的问题。
  2. 当进程收到某些信号时,如CTRL+C: 这会导致进程接收到终止信号,如 SIGINT,通常导致进程异常退出。
  3. 最后一个线程对取消请求做出响应: 线程库中的线程可能会对取消请求做出响应,从而结束线程和相关进程。

 如何使用 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 文件中。可以使用文本编辑器,例如 nanovim,来编辑这些文件:

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;
}

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值