Linux/Unix进程概念及基本操作(PID、内存布局、虚拟内存、环境变量、fork、exit、wait、exec、system)

进程

文章目录

I 进程基本概念

1、进程和程序
  • 进程是一个可执行程序的运行时实例

  • 进程是由内核定义的抽象实体,并为该实体分配用于执行程序的各种资源

2、进程号和父进程号
  • 每个进程都有一个进程号(PID),进程号是一个正数,用于唯一标识系统中的某个进程

  • 可以通过系统调用getpid()来获取当前进程的PID

c 复制代码
    #include <unistd.h>
    pid_t getpid(void);
  • 可以通过系统调用getppid()来获取当前进程的父进程PID
c 复制代码
    #include <unistd.h>
    pid_t getppid(void);
  • 如果子进程的父进程终止,那么该子进程就会变为孤儿进程,init进程(PID为1)将收养该进程,子进程后续对getppid()的调用将返回进程号1
3、进程内存布局

每个进程所分配的内存由程序段、数据段、栈和堆组成:

  • 程序段(Text) 包含了进程运行的程序机器语言指令,具有只读属性,程序段可共享给多个进程同时运行

  • 未初始化数据段(bss) 包含了未显示初始化的全局变量和静态变量,程序启动前系统会将该段内所有内存初始化为0

c 复制代码
    int a[2];/*未显示初始化的变量*/
  • 初始化数据段(InitializedData) 包含了显示初始化的全局变量和静态变量
c 复制代码
    int a[2] = {0,1};/*显示初始化的变量*/
  • 栈(Stack) 是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,用于存储函数的局部变量、实参、返回值

  • 堆(Heap) 是可在运行时动态分配的一块区域 (例如malloc())

c 复制代码
    char *p = (char *)malloc(sizeof(char)*1024);/*p指向堆内存*/
  • 命令行参数和环境变量单独存放于栈之上的一片连续的内存区域
4、虚拟内存管理
(1)程序的两种局部性

空间局部性:由于指令是顺序执行的,程序倾向于访问当前(或最近)访问过的内存附近的内存

时间局部性:由于循环,程序倾向于在不久的将来再次访问最近刚访问过的内存

(2)虚拟内存的规划
  • 虚拟内存将每个程序使用的内存分割成固定大小的页(通常4KB);相应地,物理内存被划分为相同大小的页帧;每个程序仅有部分页驻留在物理内存中,其余页保留在磁盘交换区(swap area)

  • 内核为每个进程维护页表,实现虚拟地址到物理地址的映射。页表项记录虚拟页对应的物理页帧位置(若不在内存则标记为无效并包含磁盘位置信息)

  • 当进程访问的虚拟地址对应的页不在物理内存时,会触发缺页异常(page fault),此时操作系统负责从磁盘加载所需页到内存,并更新页表

  • 虚拟内存的实现需要硬件中分页管理单元的支持,它负责将么个虚拟内存地址转换为相应的物理内存地址

(3)虚拟内存的优点
  • 扩展可用内存 :通过交换技术使程序可使用超过物理内存容量的地址空间

  • 进程隔离保护 :每个进程拥有独立地址空间,防止非法内存访问

  • 简化编程模型 :程序员无需考虑物理内存限制和布局

  • 高效内存利用 :通过按需分页和共享机制减少物理内存占用

  • 支持高级特性:为内存映射文件、写时复制等技术提供基础

5、栈和栈帧

函数调用使栈的收缩和增长呈线性;每次函数调用时,会在栈上分配一个栈帧,函数返回时再从栈上将此帧去除

每个栈帧包含以下信息:

(1)函数实参和局部变量:

存储函数调用时传入的参数以及在函数内部定义的局部变量

(2)函数调用的链接信息:

包括返回地址(调用结束后继续执行的指令地址)和调用者的栈帧指针(用于恢复调用者的栈帧)

6、命令行参数argc和argv

每个C语言程序都是从main()开始执行的,命令行的参数通过argcargv传递给main()

c 复制代码
    int main(int argc, char *argv[]){
        /*function main*/
    }
  • argc表示参数的个数

  • argv是一个指向命令行参数的指针数组,第一个指针argv[0]指向的字符串通常是该程序的名称

  • argv中的每个指针指向的参数都是一个以空字符结尾的字符串,argv指针列表以NULL结尾(argv[argc]==NULL)

7、环境变量

每个进程都有与其相关的称为环境变量的字符串数组,他们是"名称-值"的成对集合,可以存储任何信息。新进程在创建时会继承其父进程的环境副本。

使用putenv、setenv、unsetenv设置的环境变量作用域为当前进程及其子进程

(1)通过全局变量environ访问环境变量
  • 在C语言中可以通过全局变量char **environ访问环境变量
c 复制代码
    extern char **environ;
  • environ变量结构与argv类似:
  • 打印所有环境变量
c 复制代码
    extern char **environ;
    int main(){
        char **ep;
        for(ep = environ; *ep != NULL; ep++){
            printf("%s\n",*ep);
        }
        return 0;
    }
(2)通过getenv()获取环境变量
  • getenv()函数能够从环境变量中检索某个值:
c 复制代码
    #include <stdlib.h>
    char *getenv(const char *name);
    /*return pointer to stiring,or NULL if no such environment variable*/
  • 参数name是需要检索的环境变量的名称

  • 获取环境变量SHELL的值的示例:

c 复制代码
    // Get and print specific environment variable
    // argv[0] is the function name
    // argv[1] is the name of env
    int main(int argc, char *argv[]){
        // Check if argument is provided
        if(argc < 2){
            printf("usage:%s <name>\n", argv[0]);
            return -1;
        }

        // Get environment variable
        char *env = getenv(argv[1]);
        if(strcmp(env,"NULL") == 0)
            printf("no such valuable %s\n", argv[1]);
        else
            printf("the value of $%s is %s\n", argv[1], env);

        return 0;
    }
(3)通过putenv()修改环境变量
  • putenv()函数用于添加或修改环境变量:
c 复制代码
    #include <stdlib.h>
    int putenv(char *string);
    /*returns 0 on success, or nonzero on error*/
  • 参数string应为NAME=value格式的字符串

  • 不会复制传入的字符串,而是直接将其指针添加到环境变量表中 。因此,如果后续修改或释放传入的字符串,环境变量的值也会受到影响

  • 修改环境变量PATH的示例:

c 复制代码
    // Put environment variable in form "name=value" and print it if successful
    int main(int argc, char *argv[]){
        // Check if argument is provided
        if(argc < 2){
            printf("usage:%s <name=value>\n",argv[0]);
            return -1;
        }

        // Set environment variable
        if(putenv(argv[1]) != 0){
            perror("putenv");
            return -1;
        }

        // Search and print the newly set environment variable
        for(char **ep = environ; *ep != NULL; ep++){
            if(strcmp(argv[1],*ep) == 0){
                printf("%s\n", *ep);
                break;
            }
        }

        return 0;
    }
(4)通过setenv()修改环境变量
  • setenv()函数更安全地设置环境变量:
c 复制代码
    #include <stdlib.h>
    int setenv(const char *name, const char *value, int overwrite);
    /*returns 0 on success, or -1 on error*/
  • 参数name是需要修改的环境变量的名称,参数value是需修改为的值

  • 若环境变量已存在,参数overwrite为0时,不修改其值,非0时将修改值

  • 示例:

c 复制代码
    // Set environment variable with name and value, overwrite if exists
    int main(int argc, char *argv[]){
        // Check if arguments are provided
        if(argc < 3){
            printf("Usage:%s <name> <value>\n", argv[0]);
            return -1;
        }

        // Set environment variable with overwrite flag
        if(setenv(argv[1], argv[2], 1) != 0){
            perror("setenv");
            return -1;
        }

        // Print the newly set environment variable
        printf("$%s = %s\n", argv[1], getenv(argv[1]));

        return 0;
    }
(5)通过unsetenv()移除环境变量
  • unsetenv()函数用于移除环境变量:
c 复制代码
    #include <stdlib.h>
    int unsetenv(const char *name);
    /*returns 0 on success, or -1 on error*/
  • name是需要移除的环境变量的名称

  • 若需要清除所有环境变量可将全局变量environ赋值为NULL,或使用cleanenv()

  • 示例:

c 复制代码
    // Remove environment variable and verify removal
    int main(int argc, char *argv[]){
        // Check if argument is provided
        if(argc < 2){
            printf("Usage:%s <name>", argv[0]);
            return -1;
        }

        // Remove environment variable
        if(unsetenv(argv[1]) != 0){
            perror("unsetenv");
            return -1;
        }

        // Verify and print removal status
        char *get = getenv(argv[1]);
        if(get == NULL)
            printf("$%s has been cleaned!\n", argv[1]);
        else
            printf("$%s = %s", argv[1], get);

        return 0;
    }
(6)shell中的环境变量
  • 通过在自身环境列表中放置变量值,shell就可以把这些值传递给其所创建的进程,并以此来执行用户的命令
  • Linux下shell环境变量常见操作:
    • export VAR=value 设置环境变量(对当前shell及子进程有效)
    • unset VAR 删除环境变量
    • env 查看所有环境变量
    • echo $VAR 查看特定环境变量值
  • 特殊环境变量:
    • PATH 可执行文件搜索路径
    • HOME 用户主目录路径
    • SHELL 当前shell程序路径
    • USER 当前用户名
  • 持久化配置:
    • 全局配置:/etc/profile/etc/environment
    • 用户配置:~/.bashrc~/.profile

II 进程的创建

通过fork() exit() wait() execve()等系统调用对进程进行创建、退出、等待和执行其他进程等操作,他们之间的协作关系如下(shell执行一条命令的过程):

1、fork()创建进程
  • 系统调用fork()用于创建一个新的进程,创建的新进程几乎是对调用进程的翻版
c 复制代码
    #include <unistd.h>
    pid_t fork(void);
    /*In parent,return pid of child process on success or -1 on error;
      In successfully created child: always return 0 */
  • 完成对fork()的调用后将存在两个进程,且两个进程都会从fork()开始执行

  • 两个进程虽然执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝

  • fork()创建进程后父子进程都可以修改各自的栈数据以及堆中的变量

  • 程序代码中可通过fork()的返回值来区分父子进程,父进程中fork()返回子进程的pid,子进程中fork()返回0

  • 调用fork()之后,系统将调度哪个进程先试用CPU是无法确定的,这会导致竞态条件

  • 示例:

c 复制代码
    int main(){
        int a = 100;
        char b[1024] = "hello world!";

        pid_t proc = fork();
        if(proc == -1){
            perror("fork");
            return -1;
        }
        else if(proc == 0){
            a = 3;
            strcpy(b, "I am child process!");
            printf("a = %d,\n b = %s\n", a, b);
            return 0;
        }
        else{
            printf("I am parent process!\n");
            printf("a = %d,\n b = %s\n", a, b);
        } 

        return 0;
    }
2、父子进程间的文件共享
  • 执行fork()时,子进程会获得父进程所有的文件描述符的副本,父子进程中对应的描述符均指向相同的打开文件句柄;

  • fork()使子进程继承了父进程的文件属性,包括文件偏移量、文件属性等,如果子进程更新了文件属性,那么父进程中的文件属性也将随之更新

  • 示例:在父进程中打开文件,在子进程中验证是否继承属性并写入内容,在父进程中读取子进程写入的内容

c 复制代码
    int main(int argc, char *argv[]){
        if(argc < 2){
            printf("Usage:%s <file_name>\n", argv[0]);
            return -1;
        }

        // Open file with read-write permissions, create if doesn't exist
        int fd = open(argv[1], O_RDWR | O_CREAT, 0644);
        if (fd == -1){
            perror("open");
            return -1;
        }

        // Create child process
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            close(fd);
            return -1;
        } 
        
        if(pid == 0) {
            // Verify child process inherits file attributes from parent process 
            if(fcntl(fd, F_GETFL) & O_RDWR) {
                printf("File is opened with O_RDWR\n");
            }
            // Child process writes to file
            char str[] = "<this is child>";
            if(write(fd, str, strlen(str)) == -1){
                perror("Child write");
            };
        }
        
        if(pid > 0){
            sleep(1);  // Wait for child to complete writing
            lseek(fd, 0, SEEK_SET);  // Reset file pointer to beginning
            // Parent process reads from file after child writes
            char buf[1024];    
            if(read(fd, buf, 1024) == -1){
                perror("read");
                close(fd);
                return -1;
            }
            printf("parent read from file %s :\n %s\n", argv[1], buf);
        }
        
        close(fd);  // Close file descriptor
        return 0;
    }
3、fork()的内存语义
  • 内核将每一进程的代码段标记为只读,父子进程可共享同一段代码;系统调用fork()在为子进程创建代码段时,其所构建的一系列进程级页表项均指向与父进程相同的物理内存页帧;

  • 对于数据段、堆段和栈段,内核采用写时复制(CoW)技术:初始时,它们的页表项指向与父进程相同的物理页。当任一进程尝试修改共享页时,内核会捕获写操作,为修改的进程分配新的物理页,并更新页表,这时父子进程便可以互不干扰的分别修改各自的页帧。

4、同步信号以规避竞争条件
  • 竞争条件:调用fork()后,父子进程的执行顺序由内核调度器决定,这种不确定性可能导致对共享资源(如文件描述符、内存等)的访问出现竞态条件,从而影响程序行为的正确性

  • 使用信号同步 :父进程可以通过信号(如SIGUSR1)来通知子进程开始执行,从而避免竞争

  • 示例:父进程等待子进程准备就绪后发送信号,子进程收到信号后再执行关键代码

c 复制代码
    sig_atomic_t p_sync = 0;  // Atomic flag for process synchronization

    void handler(){
        p_sync = 1;  // Signal handler sets sync flag
    }

    int main(){
        signal(SIGUSR1, handler);  // Register signal handler
        
        pid_t pid = fork();
        if(pid == -1){
            perror("fork");
            return -1;
        }

        if(pid == 0){
            // Child waits for signal
            while(!p_sync){
                pause();  // Sleep until signal received
            }
            printf("child process executing!\n");
        }

        if(pid > 0){
            printf("parent process preparing!\n");
            sleep(1);  // Give child time to setup
            kill(pid, SIGUSR1);  // Send signal to child
        }

        return 0;
    }

III 进程的终止

1、main函数return

进程终止的一种方法是在进程的main函数中通过return,或者执行到main函数结尾,调用main的运行时函数会将main函数的返回值作为exit()的参数

2、系统调用_exit()
  • 进程可以调用_exit()系统调用正常终止
c 复制代码
    #include <unistd.h>
    void _exit(int status);
  • 参数status定义了进程的终止状态,父进程可以调用wait()来获取该状态,终止状态为0表示成功退出,终止状态为非0值则表示异常退出
3、库函数exit()
  • exit()对系统调用_exit()进行了封装,在执行_exit()之前会执行一系列操作
c 复制代码
    #include <stdlib.h>
    void exit(int status);
  • exit执行的操作包括:

(1)、调用退出处理函数(由atexit()on_exit()注册的函数),执行顺序与注册顺序相反

(2)、刷新stdio流缓冲

(3)、使用由status提供的值执行系统调用_exit()

4、函数atexit()
  • atexit函数用于注册退出处理函数
c 复制代码
    #include <stdlib.h>
    int atexit(void (*func)(void));
    /*return 0 on success or nozero on error*/
  • 参数func指向需注册的函数,atexit()会将其添加到一个函数列表中,进程终止时会调用该函数列表的所有函数,func()函数应为无参数,无返回值的形式:
c 复制代码
    void func(void){
        /*perform some actions*/
    }
  • atexi()注册的退出处理函数会有两种限制:无法获知传递给exit()的状态,无法给给退出处理程序指定参数

  • 示例:处理函数的执行顺序与注册顺序相反

c 复制代码
    // Function to be called at exit
    void func_atexit_01(){
        printf("func_atexit 01 is called\n");
    }

    // Function to be called at exit
    void func_atexit_02(){
        printf("func_atexit 02 is called\n");
    }

    // Register exit handler and demonstrate execution order
    int main(){
        // Register exit handler function
        if(atexit(func_atexit_01) != 0){
            printf("atexit error\n");
        };

        if(atexit(func_atexit_02) != 0){
            printf("atexit error\n");
        };
        
        // Main function execution
        printf("hello, this is the main function\n");
        return 0;
    }
5、函数on_exit()
  • 函数on_exit()同样用于注册退出处理函数
c 复制代码
    #define _BSD_SOURCE
    #inlcude <stdio.h>
    int on_exit(void (*func)(int, void*), void *arg)
    /*return 0 on success or nozero on error*/
  • func是指向退出处理函数的指针,可带参数:
c 复制代码
    void func(int status, void *arg){
        /*perform some actions*/
    }

status为提供给exit()的参数,argv为提供给on_exit()的一份参数拷贝

  • 示例:
c 复制代码
    void func_onexit_01(int status, void *arg){
        printf("onexit func 01 is called\n");
        printf("status = %d, arg = %s\n", status, (char *)arg);
    }

    void func_onexit_02(int status, void *arg){
        printf("onexit func 02 is called\n");
        printf("status = %d, arg = %s\n", status, (char *)arg);
    }

    int main(){
        if(on_exit(func_onexit_01, "first") != 0)
            printf("onexit error\n");

        if(on_exit(func_onexit_02, "second") != 0)
            printf("onexit error\n");

        printf("hello, this is the main function\n");
        return 0;
    }
6、进程终止的细节
  • 进程终止时内核会执行以下操作

    (1)、关闭所有打开的文件描述符、目录流、消息队列描述符等

    (2)、释放进程持有的文件锁、共享内存等资源

    (3)、将子进程的父进程改为init进程(PID=1)

    (4)、向父进程发送SIGCHLD信号

    (5)、将进程状态置为僵尸状态(Zombie),直到父进程调用wait()获取终止状态

  • 异常终止的情况

    (1)、进程收到未处理的信号(如SIGSEGV、SIGKILL)

    (2)、进程调用abort()触发SIGABRT信号

    (3)、最后一个线程执行pthread_exit()

7、fork()、stdio缓冲区以及_exit()之间的交互
  • 调用fork()时,子进程会继承父进程的stdio缓冲区副本

  • 如果父进程在调用fork()前有未刷新的缓冲区数据(例如printf输出),这些数据会被复制到子进程

  • 示例:

c 复制代码
    int main(){
        printf("hello world\n");

        write(STDOUT_FILENO, "Fison\n", 6);

        if(fork() == -1){
            perror("fork");
            return -1;
        }

        return 0;
    }

输出为:

shell 复制代码
    $ ./main
        hello world
        Fison
    $ ./main > ./tmp
    $ cat tmp
        Fison
        hello world
        hello world

两个输出不一样的原因

(1)、write()是系统调用,直接写入文件描述符,不经过缓冲区

(2)、printf()使用stdio缓冲区,默认行缓冲模式(终端)会立即刷新,全缓冲模式(重定向到文件)不会立即刷新

(3)、fork()时未刷新的stdio缓冲区会被复制到子进程,导致父子进程各刷新一次缓冲区

  • 为防止以上情况,可以

    • Call fflush(NULL) before fork() to flush all stdio buffers.
    • Use _exit() in child processes to avoid flushing buffers twice.

IV 监控子进程

1、系统调用wait()
  • 系统调用wait()等待调用进程的任一子进程终止,同时在status所指的缓冲区返回该子进程的终止状态
c 复制代码
    #include <sys/wait.h>
    pid_t wait(int *status);
    /*return process id of terminated child or -1 on error*/
  • wait()将一直阻塞,直达某个子进程终止,如果调用时已经有子进程终止,wait()会立即返回

  • status非空时,子进程的终止信息将通过其指向的指针返回

  • wait()会返回终止子进程的ID

  • wait()的缺点:

(1)、无法等待某个特定的子进程完成

(2)、没有子进程退出,则一直保持阻塞

(3)、只能发现已经终止的进程,无法发现因某个信号终止的进程或恢复执行的进程

  • 示例:
c 复制代码
    int process_wait(int argc, char *argv[]){
        // Check if correct number of arguments is provided
        if(argc < 2){
            printf("Usage:%s <number to fork>\n", argv[0]);
            return -1;
        }

        pid_t pid;
        // Fork multiple child processes based on input argument
        for(int i = 0; i < atoi(argv[1]); i++){
            pid = fork();
            if(pid == -1){
                perror("fork");
                return -1;
            }

            // Child process execution
            if(pid == 0){
                printf("child %ld is created\n", (long int)getpid());
                sleep(1);  // Simulate work
                exit(EXIT_SUCCESS);  // Exit successfully
            }
        }

        // Parent process waits for all children to terminate
        pid_t child;
        while(1){
            child = wait(NULL);  // Wait for any child to terminate
            if(child == -1){
                if(errno == ECHILD){  // No more children left
                    printf("all child terminated\n");
                    break;
                } 
                else{  // Other error occurred
                    printf("wait error\n");
                    continue;
                }
            }
            printf("child %ld terminated successfully\n", (long int)child);
        }

        return 0;
    }
2、系统调用waitpid()
  • 系统调用waitpid()用于等待IDpid的子进程终止,并通过参数status返回状态信息
c 复制代码
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *status, int optinos);
    /*return process id of child, 0(...), or -1 on error*/ 
  • 参数pid可以为任意整数:

pid大于0,表示等待ID为pid的进程终止

pid等于0,表示等待与父进程同一个进程组的所有子进程

pid小于-1,表示等待进程组标识符与pid绝对值相同的所有子进程

pid等于-1,表示等待任意进程,与wait()等价

  • 参数option是一个位掩码,可以按位或,其值可以是:

WUNTRACED-除了返回终止子进程信息外,还返回因信号而停止的子进程信息

WCONTINUED-返回那些因收到SIGCONT

WNOHANG如果pid所指定的子进程并未发生状态变化,则立即返回,而不会阻塞

3、等待状态值status
  • WIFEXITED(status)--若子进程真诚结束则返回真

  • WIFSIGNALED(status)--若通过信号杀掉子进程则返回真

  • WIFCONTINUED(status)--若子进程收到SIGCONT而恢复执行则返回真

  • WIFSTOPPED(status)--若子进程因信号而终止则返回真

  • 示例:waitpid()status

c 复制代码
    int main(){
        // Create child process
        pid_t pid = fork();
        if(pid == -1){
            perror("fork");
            return -1;
        }

        // Child process execution
        if(pid == 0){
            printf("this is child\n");
            exit(EXIT_SUCCESS);
        }
        
        // Parent process waits for child with status checking
        int status;
        pid_t w_child = waitpid(pid, &status, WUNTRACED); 
        if( w_child == -1){
            perror("waitpid");
            return -1;
        }
        else{
            printf("child %ld terminted\n", (long int)w_child);
        }

        // Check termination status
        if(WIFEXITED(status))
            printf("child termined normally\n");

        if(WIFSIGNALED(status))
            printf("child terminated by signal\n");

        return 0;
    }
4、孤儿进程与僵尸进程
  • 孤儿进程(Orphan Process)

父进程先于子进程终止时,子进程成为孤儿进程

孤儿进程会被init进程(PID=1)收养

不会对系统造成危害

  • 僵尸进程(Zombie Process)

子进程终止后,父进程尚未调用wait()/waitpid()获取其终止状态

进程表中仍保留子进程的退出状态等信息

会占用系统资源(进程表项)

大量僵尸进程会导致系统无法创建新进程

  • 处理方法

父进程调用wait()/waitpid()主动回收

父进程忽略SIGCHLD信号(可能导致无法获取子进程状态)

杀死父进程(孤儿进程会被init接管并自动回收)

V 进程的执行

1、系统调用execve()
  • 系统调用execve()可以将程序加载到某一进程的内存空间,进程的栈、数据段以及堆将会被新程序的相应部分所替换
c 复制代码
    #include <unistd.h>
    int execve(const char *pathname, char *const argv[], char *const envp[]);
    /*never return on success, return -1 on error*/
  • 参数pathname是要执行的可执行文件的路径名,可以是绝对路径也可以是相对路径

  • 参数argv是指向传递给新程序的参数列表的指针数组,以NULL结尾

  • 参数envp是指向新程序环境变量的指针数组,以NULL结尾

  • execve()的调用将永不反悔,而且也无需检查其返回值,一旦函数返回,就表明发生了错误,可以通过errno来判断出错的原因

  • 示例:

c 复制代码
    int process_exec(int argc,char *argv[]){
        if(argc < 3){
            printf("Usage: %s <executablefile> <arguements>", argv[0]);
            return -1;
        }

        char *envp[] = {"PATH=/bin:/usr/bin:/usr/local/bin", NULL};
        if (execve(argv[1], &argv[1], envp) == -1){
            perror("execve");
            return -1;
        }

        // This line will never be reached if execve succeeds
        printf("After execve\n");
        return 0;
    }
2、exec系库函数
  • 基于execve()系统调用,标准C库提供了多个exec函数变体:
c 复制代码
    #include <unistd.h>
    int execl(const char *path, const char *arg, ..., NULL);
    int execlp(const char *file, const char *arg, ..., NULL);
    int execle(const char *path, const char *arg, ..., NULL, 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[]);
  • 函数命名规则:

l-参数以列表形式传递

v-参数以数组形式传递

p-使用PATH环境变量查找可执行文件

e-可自定义环境变量

  • 示例:不同exec函数的使用
c 复制代码
    // execl示例
    execl("/bin/ls", "ls", "-l", NULL);

    // execv示例
    char *argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);

    // execlp示例
    execlp("ls", "ls", "-l", NULL);

    // execle示例
    char *envp[] = {"PATH=/bin", NULL};
    execle("/bin/ls", "ls", "-l", NULL, envp);

    // execvp示例
    char *argv_vp[] = {"ls", "-l", NULL};
    execvp("ls", argv_vp);

    // execvpe示例
    char *argv_vpe[] = {"ls", "-l", NULL};
    char *envp_vpe[] = {"PATH=/bin", NULL};
    execvpe("ls", argv_vpe, envp_vpe);
3、通过system()执行shell指令
  • system()函数提供了一种执行shell命令的简单方式
  • 该函数会创建一个子进程来执行指定的命令,并等待其完成
c 复制代码
    #include <stdlib.h>
    int system(const char *command);
    /* Returns termination status of shell or -1 on error */
  • 参数command是要执行的shell命令字符串

  • 返回值:

    • 成功时返回命令的终止状态
    • 如果无法创建子进程或无法获取状态则返回-1
    • 如果command为NULL,返回非零值表示shell可用
  • 实现原理:

    • 调用fork()创建子进程
    • 子进程调用execl("/bin/sh", "sh", "-c", command, NULL)执行命令
    • 父进程调用waitpid()等待子进程结束
  • 示例:

c 复制代码
    int ret = system("ls -l");
    if (ret == -1) {
        perror("system");
        return -1;
    }
    else if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0) {
        printf("Command failed with exit status %d\n", WEXITSTATUS(ret));
    }
相关推荐
IC 见路不走26 分钟前
LeetCode 第91题:解码方法
linux·运维·服务器
翻滚吧键盘38 分钟前
查看linux中steam游戏的兼容性
linux·运维·游戏
小能喵43 分钟前
Kali Linux Wifi 伪造热点
linux·安全·kali·kali linux
汀沿河1 小时前
8.1 prefix Tunning与Prompt Tunning模型微调方法
linux·运维·服务器·人工智能
zly35001 小时前
centos7 ping127.0.0.1不通
linux·运维·服务器
小哥山水之间2 小时前
基于dropbear实现嵌入式系统ssh服务端与客户端完整交互
linux
ldj20202 小时前
2025 Centos 安装PostgreSQL
linux·postgresql·centos
翻滚吧键盘2 小时前
opensuse tumbleweed上安装显卡驱动
linux
黑听人3 小时前
【力扣 简单 C】70. 爬楼梯
c语言·leetcode
杜子不疼.3 小时前
二分查找,乘法口诀表,判断闰年,判断素数,使用函数实现数组操作
c语言