一、进程创建

fork(创建进程)

Linuxfork函数是非常重要的函数,它从已存在进程中创建一个新进程。

新进程为子进程,而原进程为父进程

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

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

进程调用fork之后,当控制转移到内核中的fork代码后,内核会做:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

一个进程调用fork之后,会有两个二进制代码相同的进程,而且它们都运行到相同的地方

但每个进程都将可以开始属于它们自己的旅程

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

int main()
{
 pid_t pid;
 printf("Before: pid is %d\n", getpid());
 if ( (pid=fork()) == -1 ) perror("fork()"),exit(1);
 printf("After:pid is %d, fork return %d\n", getpid(), pid);
 sleep(1);
 return 0;
}

输出结果

进程1348063先打印before消息,然后它再打印after。另一个after消息有1348064打印的
但是意到进程1348064没有打印before,为什么呢?

fork之前父进程独立执行,fork之后,父子两个执行流分别执行
注意,fork之后,谁先执行完全由调度器决定

写时拷贝

父子代码共享,父子在不写入时,数据也是共享的
当任意一方试图写入,便以写时拷贝的方式各自一份副本

当父进程形成子进程之后,子进程正在写入,如何进行写时拷贝?
重新申请空间,进行拷贝,修改页表(os)
原因:父进程创建子进程的时候首先将自己的读写权限,改为只读,然后再创建子进程,但是用户他是不知道的,用户可能会对某一些数据进行写入,这时页面转换会因为权限问题报错,不过不是真的出错,而是触发我们进行申请内存拷贝内容的策略机制,然后操作系统就可以介入了

fork常规用法
  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段
  • 例如,父进程等待客户端请求,生成子进程来处理请求
  • 一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数
fork调用失败的原因
  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

二、进程终止

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

进程常见退出方法

正常终止(通过 echo $? 查看进程退出码,?保存的是最近一个子进程执行完毕时的退出码):
  • 从main返回

  • 在main函数直接return
  • 在其他函数中进行return,表示的是函数调用结束
  • main函数返回值,叫做进程的退出码(0表示成功,非0表示失败)
  • 调用exit

从上述代码看出printf没有实现可以说明任意地点调用exit,表示进程退出,不进行后续执行

参数就是进程的退出码,类似于main return n

  • _exit
异常退出
ctrl + c,信号终止
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
  • 执行用户通过 atexit或on_exit定义的清理函数
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit

用例:

int main()
{
    printf("aaaaaa");
    exit(31);
}

int main()
{
    printf("aaaaaa\n");
    _exit(31);
}

  • 1.exit是库函数,_exit是系统调用
  • 2.exit终止进程的时候,会自动刷新缓冲区。_exit终止进程的时候,不会自动刷新缓冲区

三、进程等待

 什么是进程等待

通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程

为什么要进行等待

  • a.解决子进程僵死问题带来的内存泄露
  • b.父进程为什么要创建子进程?要让子进程来完成任务。子进程任务完成的如何,父进程要不要知道?要知道的话需要通过进程等待的方式,获取子进程退出的信息,虽然不是必须的,但是系统需要提供这样的基础功能

进程等待必要性

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就kill -9 也无法解决,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源获取子进程退出信息

进程等待的方法

wait

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid

pid_ t waitpid(pid_t pid, int *status, int options);

    WNOHANG是waitpid 函数的一个选项,表示非阻塞模式

    WNOHANG 的本质是提供了一个非阻塞的(non-blocking)选项,使得父进程可以以“轮询”的方式检查子进程的状态,而不会因为等待子进程而停止自身的执行。 这对于提高程序的响应性和并发处理能力至关重要

    返回值
    当正常返回的时候waitpid返回收集到的子进程的进程ID
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
    pid
    • pid=-1,等待任一个子进程。与wait等效
    • pid>0.等待其进程IDpid相等的子进程
    status
    • WIFEXITED(status): 若正常终止子进程返回的状态则为真(查看进程是否是正常退出)
    • WEXITSTATUS(status): WIFEXITED非零,提取子进程退出码(查看进程的退出码)
    options
    WNOHANG: pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
    #include<stdio.h>
    #include <stdlib.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    void work()
    {
        int cnt=5;
        while(cnt)
        {
            printf("I am child process,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
        }
        
    }
    
    int main()
    {
        pid_t id=fork();
        if(id==0)
        {
            work();
            exit(0);
        }
        else{
            printf("wait before\n");
            pid_t rid=wait(NULL);
            printf("wait after\n");
            if(rid==id)
            {
                printf("wait success,pid:%d\n",getpid());
            }
        }
        sleep(10);
    }
    • rid>0:等待成功
    • rid==0:等待是成功的,但是可能对方没有成功
    • rid<0:等待失败

    从结果可以知道子进程没有退出,父进程就必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收返回,也就是说wait after必须等待子进程结束才会执行父进程代码

    • 如果子进程已经退出,调用wait/waitpid时,会立即返回并且释放资源获得子进程退出信息。
    • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
    • 如果不存在该子进程,则立即出错返回

     获取子进程status

    • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
    • 如果传递NULL,表示不关心子进程的退出状态信息。
    • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
    • status不能简单的当作整形来看待,可以当作位图来看待

      

    exit sig(退出信号):status&0x7F  exit code(退出码):(status->8)&0xFF

    只考虑status低16比特位,次8位子进程退出的退出码,低7位代表子进程退出的推出信号

    1.那为什么不用全局变量获取子进程退出信息?而是系统调用

    进程具有独立性,父进程无法直接获取子进程退出信息

    2.当一个进程异常了(收到信号exit sig),exit code就无意义了

    如何判断有没有收到信exit sig=0

    四、进程程序替换

    替换原理

    fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

    替换函数

    六种以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 execve(const char *path, char *const argv[], char *const envp[]);

    函数解释

    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
    • 如果调用出错则返回-1
    • 所以exec函数只有出错的返回值而没有成功的返回值

    命名理解

    其实这六个函数所有的接口都是对execvec的封装,底层都是execvec

    下面看一下exec函数的使用,拿execl来说

    path:要替换哪一个程序->文件->程序文件的路径+文件名

    arg:如何执行问题?命令行怎么写->就将参数怎么传

    发现最后一个printf没有实现这是怎么回事?

    原因:exec*这样的函数,如果当前进程执行成功,则后续代码没有机会再执行,因为被替换掉了

    1.进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的?

    父进程添加环境变量会被子进程继承下去

    2.环境变量被子进程继承下去是一种默认行为,不受程序替换影响,为什么呢?

    通过地址空间可以让子进程继承父进程的环境变量数据

    Logo

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

    更多推荐