一、进程创建

1.fork函数

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

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

作用:从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程

利用fork函数返回值的不同,执行不同的任务

eg.

#include<stdio.h>
#include<unistd.h>
#include<assert.h>

int main()
{
    pid_t id = fork();
    assert(id >= 0);
    //子进程 
    if(id==0)
    {
        //...
    }
    //父进程
    else
    {
        //...
    }
}

2.fork函数返回值问题

a.如何理解:子进程中返回0,父进程返回子进程pid

子进程有唯一的父进程,而父进程可能有多个子进程,需要确定唯一的子进程

b.如何理解:fork函数有两个不同的返回值

当fork函数内部准备retrun时,子进程已经创建好了,由于父子进程代码共享,所以父子进程各自执行return

c.如何理解:同一个id变量,保存两个不同的fork返回值

因为接收返回值的本质就是写入,所以会触发写时拷贝

现象:父子进程的两个id变量的虚拟地址相同,值却不同

3.写时拷贝

父子进程代码共享,父子进程在不写入时,数据也是共享的,当任意一方试图写入,便会在物理内存上新开一块区域,拷贝数据,再让进程进行修改

此时虚拟地址未改变,而通过各自页表映射的物理地址已经发生改变

如图:

当子进程的value变量发生改变时

二、进程终止

1.进程退出码

退出码也称为返回码或状态码,是程序在终止时返回给操作系统的一个整数值

比如,进程正常终止时,main函数中的return值

a.查看退出码

echo $?

$?:记录最后一个进程在命令行中执行完毕的退出码

eg.

mytest进程的退出码是1,而执行的echo命令本身也是一个进程,退出码是0

b.退出码的作用

退出码标定程序的执行结果是否正确

特定的数字表示特定的错误,用0表示结果正确,非0表示错误,并且不同的数字表示的错误不同

一般而言,不同的退出码有对应的文字描述,而对应的映射关系可以是自定义,也可以是系统默认

c.将退出码转化为文字描述

这是系统默认的退出码与文字描述之间的映射关系

#include <string.h>
char* strerror(int errnum);

2.进程三种退出场景

a.代码正常终止

代码运行完毕,结果正确

代码运行完毕,结果不正确(退出码起作用)

b.代码异常终止(退出码无意义)

3.进程退出方式

a.main函数return返回

b.在任意地方调用exit或_exit函数,都会立即终止进程并退出

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

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

exit是库函数,_exit是系统调用接口,exit是通过调用_exit实现,并且增加主动刷新缓冲区的功能

举例:

#include <stdio.h>
#include <stdlib.h>                                                                                

int main()
{
    printf("hello");
    exit(0);
}

运行结果:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("hello");
    _exit(0);                                                                                      
}

运行结果:

三、进程等待

1.进程等待的必要性

父进程通过进程等待的方式:a.回收子进程资源 b.获取子进程退出信息

2.进程等待的方法

a.wait方法

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

返回值:成功返回被等待进程pid,失败返回-1

参数:输出型参数,可获取子进程退出状态,不关心则可以设置成为NULL

b.waitpid方法

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

参数:

1)pid:     

  • pid=-1,等待任意一个子进程(与wait等效)
  • pid>0,等待其进程ID与pid相等的子进程

2)status:

  • 输出型参数,获取子进程退出状态

3)options:                                                                        

  • 0:阻塞等待
  • WNOHANG:非阻塞等待 

4)返回值: 

  • 阻塞等待:等待成功,返回收集到的子进程的ID;waitpid()函数调用中出错,等待失败,返回-1。
  • 非阻塞等待:若pid指定的子进程没有结束,返回0;子进程已经结束,则返回子进程的ID;waitpid()函数调用中出错,等待失败,返回-1。   

c.status参数

1)status参数介绍

wait和waitpid中都有一个输出型参数status

当子进程退出后,会变成僵尸进程,此时,该进程的所有资源会被释放,但会保留进程的task_struct,这个结构体里保留了子进程退出的信息,这些信息直到父进程通过 wait()/waitpid() 来取时才释放

操作系统根据子进程的task_struct填充status

2)status参数结构

3)status参数使用方式

直接使用:

exit_signal(退出信号/错误码): status & 0x7F
exit_code(退出码): (status>>8) & 0xFF

调用函数使用:

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

4)status参数使用举例(阻塞等待)

进程正常退出:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    int id=fork();                                                                                 
    if ( id == -1 )
    {
        perror("fork");
       exit(1);
    }
   //子进程
    if ( id == 0 )
    {
        printf("I am child,pid:%d\n",getpid());
        sleep(1);
        //退出码为10
        exit(10);
    } 
    //父进程
    else 
    {
        int status;
        int ret = wait(&status);
        printf("wait successfully:%d\n",ret);
        printf("signal code:%d,child exit code:%d\n",(status & 0x7F),(status>>8) & 0xFF);          
    }
}

进程异常退出:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    int id=fork();                                                                                 
    if ( id == -1 )
    {
        perror("fork");
       exit(1);
    }
   //子进程
    if ( id == 0 )
    {
        printf("I am child,pid:%d\n",getpid());
        sleep(1);
        //野指针问题,会导致子进程异常退出
        int* p = NULL;
        *p = 100;
        //退出码为10
        exit(10);
    } 
    //父进程
    else 
    {
        int status;
        int ret = wait(&status);
        printf("wait successfully:%d\n",ret);
        printf("signal code:%d,child exit code:%d\n",(status & 0x7F),(status>>8) & 0xFF);          
    }
}

3.阻塞等待和非阻塞等待

a.阻塞等待(Blocking Wait)

阻塞等待意味着父进程会一直等待,直到子进程结束或出现错误为止。                                         

通常使用 wait() 或带有默认选项的 waitpid() 函数实现(即 参数 options = 0):waitpid(pid, &status, 0);

b.非阻塞等待(Non-blocking Wait)

非阻塞等待允许父进程在没有子进程结束的情况下继续执行其他任务。

如果子进程未退出,父进程会直接读取子进程的状态并立即返回,然后接着执行后面的语句,不会等待子进程退出

通常使用带有 WNOHANG 选项的 waitpid() 函数实现(即 参数 options = WNOHANG):waitpid(pid, &status, WNOHANG);

c.非阻塞轮询

轮询是指父进程在非阻塞式状态的前提下,以循环方式不断的对子进程进行进程等待,直到子进程退出

非阻塞轮询举例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
void task1() 
{
    printf("task1 is running...\n");
}
 
void task2() 
{
    printf("task2 is runnning...\n");
}
 
int main() 
{
    int id = fork();
    //子进程创建失败
    if(id == -1)
    {
        printf("fork error\n");
        exit(-1);
    }
    //子进程
    else if(id == 0)  
    { 
        int cnt = 5;
        while(cnt--) 
        {
            printf("子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        exit(1);
    } 
    //父进程
    else 
    {  
        int status = 0;
        //轮询
        while(1) 
        {  
            pid_t ret = waitpid(id, &status, WNOHANG);  //非阻塞式等待
            if(ret == -1) //waitpid()函数调用失败
            {
                printf("wait fail\n");  
                exit(1);
            } 
            else if(ret == 0)//调用成功,但子进程未退出
            {  
                printf("wait success, but child process not exit\n");
                //执行其他命令
                task1();  
                task2();
            } 
            else //调用成功,子进程退出
            {  
                printf("wait success, and child exited\n");
                break;
            }
            sleep(1);
        }

        //正常退出
        if(WIFEXITED(status))
        { 
            printf("exit code:%d\n", WEXITSTATUS(status));
        } 
        //异常终止
        else 
        { 
            printf("exit signal:%d\n",(status & 0x7f));
        }
        
    }
    return 0;
}

d.总结

阻塞等待:先等你,我暂停

非阻塞等待:不等你,我继续做自己的事

非阻塞轮询:不等你,我继续做自己的事,期间不断问你行没行

四、进程程序替换

1.进程替换的目的

程序替换就是将指定的程序加载到内存中,让指定进程执行

以上创建的子进程都是执行父进程代码中的一部分,而子进程程序替换是要让子进程执行磁盘上指定的程序

2.进程替换的原理

进程原来的代码和数据替换为新程序的代码和数据(新程序可以为任意语言所编写的可执行程序)

如图:

由于进程的独立性,当子进程进行程序替换时,会发生写时拷贝,不会影响父进程

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[]);

int execvpe(const char *file, char *const argv[], char *const envp[]);

以上的库函数都是通过封装系统调用execve实现的

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

命名理解:

l(list) : 参数依次直接传入
v(vector) : 所有参数放入数组中,整体传入
p(path) : 不用传入程序的路径,传入程序名,会自动在环境变量PATH的路径中搜索
e(env) : 需要自己传入环境变量,给新程序使用

2)函数说明

返回值:

如果调用成功,则加载新的程序从启动代码开始执行,不再返回;

如果调用出错,则返回-1

所以,exec函数只有出错的返回值而没有成功的返回值

效果:

exec函数执行完毕后,代码已经被完全覆盖,开始执行新的程序代码,返回值无意义;反之,调用失败,代码不会被覆盖,返回-1

3)函数的使用

所有exec函数的执行参数必须以NULL结尾

#include <unistd.h>
int main()
{
    // 带l的,需要将执行参数依次传入
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    
    // 带p的,无需写路径,只需写程序名,可以在环境变量PATH中搜索
    execlp("ls", "ls", "-a", "-l", NULL);
    
    // 带v的,将执行参数传入数组,再整体传入
    char *const argv[] = {"ls", "-a", "-l", NULL};
    execv("/usr/bin/ls", argv);
    
    // 带v,带p的
    char *const argv[] = {"ls", "-a", "-l", NULL};
    execvp("ls", argv);

    //带e的,需要传入环境变量给新程序使用
    //自定义的环境变量表(以NULL结尾),不包含默认环境变量
    char* const env[] = {(char*) "MYENV=111",NULL};
    execle("/usr/bin/ls", "ls", "-a", "-l", NULL, env);
    //默认环境变量表,只包含默认环境变量
    extern char** environ;
    execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
    //利用putenv()函数,将自定义的环境变量导入到默认环境变量表中
    //包含自定义的环境变量和默认环境变量
    putenv((char*) "MYENV=111");
    extern char** environ;
    execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
    
    exit(0);
}

五.进程控制的应用(shell的模拟实现)

一般的命令都是通过shell创建子进程来实现的

内置/内建命令:不需要创建子进程,直接由shell自己执行的命令。比如cd命令,echo命令

了解:为什么cd命令是内置命令?

cd命令是要改变shell程序(父进程)的工作目录。所以,如果创建子进程执行cd命令,就只会改变子进程的工作目录,而对父进程无影响

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

#define NUM 1024
#define OPTION_NUM 64

char lineCommand[NUM];//存放整个命令行
char* myargv[OPTION_NUM];//存放切割后的命令行参数
int status = 0;

int main()
{
    while(1)
    {
        //输出提示符
        printf("[用户名@主机名 当前路径]$");
        fflush(stdout);//刷新缓冲区
        
        //获取用户输入
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        s = NULL;//防止野指针

        //消除用户最后输入的\n
        lineCommand[strlen(lineCommand)-1] = 0;
        
        //切割命令行字符串,使其变为多个命令行参数
        myargv[0] = strtok(lineCommand, " ");//以空格为分隔符切割
        int i = 1;
        //如果没有可以分割的子串了,strtok()返回NULL,此时myargv[end]=NULL
        while(myargv[i++] = strtok(NULL, " "));
        
        //执行内置命令,如cd,echo命令
        //执行cd命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL)
            {
                chdir(myargv[1]);//直接改变父程序的工作路径
                continue;//无需再创建子进程执行命令
            }
        }
        //执行echo命令
        if(myargv[0] != NULL && myargv[1] !=NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d\n",(status>>8) & 0xFF);
            }
            else
            {
                printf("%s\n",myargv[1]);
            }
            continue;//无需再创建子进程执行命令
        }

        //创建子进程执行命令
        pid_t id = fork();
        assert(id != -1);
        //通过子进程替换,执行命令
        if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);
        }
        //阻塞等待,获取子进程的退出结果
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
    }
}

优化:使用popen函数

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

参数说明:

command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令,比如sh -c ls

type: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。

返回值:
如果调用 fork() 或 pipe() 失败,或者不能分配内存将返回NULL,否则返回一个打开文件的指针。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
int main()
{
    char lineCommand[1024] = {0};
    while (true)
    {
        // 输出提示符
        printf("[用户名@主机名 当前路径]$");
        fflush(stdout); 

        // 获取用户输入
        char *s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL);
        s = NULL;

        lineCommand[strlen(lineCommand) - 1] = 0;// 去掉换行符

        FILE *fp; // 文件指针

        // 执行命令
        fp = popen(lineCommand, "r");// 以读的方式执行命令,获取命令执行后的输出结果
        if (fp == NULL)
        {
            perror("popen error");
            continue;
        }

        // 读取命令执行结果
        char ret[1024] = {0};
         int nread = fread(ret, 1, 1024, fp);// 读取命令执行结果
         if (nread > 0)
         printf("%s\n", ret);

        pclose(fp);// 关闭文件指针
    }
    return 0;
}

运行结果:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐