【Linux】进程控制

1、进程创建

1.1 fork函数

在Linux中,fork函数是非常重要的函数,新进程为子进程,而原进程为父进程

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork后,当控制转移到内核中的 fork 代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork 返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程

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

1.2 fork函数的返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid

1.3 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

父进程在创建子进程时,操作系统会把父子进程页表中数据设置成只读,所以父子只要有一方想对数据进行写入,那么一定会触发权限问题,操作系统会把数据拷贝一份,然后把只读设置为可读可写

因为有写时拷贝技术的存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证

写时拷贝是⼀种延时申请技术,可以提高整机内存的使用率

1.4 fork的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

1.5 fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2、进程终止

2.1 进程终止的3中场景

  • 代码正常跑完,结果正确

main函数执行到末尾,执行return 0 ,全程无异常,无信号杀死

  • 代码正常跑完,结果不正确

代码完整执行完毕,逻辑出错主动返回错误码,return返回一个不为0的数

  • 代码没跑完,进程异常终止

核心原因:被操作系统信号杀死,此时退出码没有意义

当代码运行完毕,如果结果不正确,程序要将运行结果不正确的原因显示出来,这就是main函数中return的作用,return后面的数字是程序的退出码,0表示成功无错误,不同的数字表示不同的错误原因,利用echo $ 指令可以查看上一个进程的退出码

strerror函数:利用strerror函数可以查看函数的退出码对应的信息

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    int i = 0;
    for (; i < 200; i++)
    {
        printf("%d->%s\n", i, strerror(i));
    }
    return 0;
}

errno变量:errno是C语言提供的一个变量,里面保存的是最近一次执行的错误码

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main()
{
    FILE *fp = fopen("./logtxt", "r"); // 打开文件失败
    if(fp == NULL)
    {
        printf("%d: %s\n", errno, strerror(errno));
    }
    return 0;
}

2.2 进程的退出方法

  • main函数内部return
  • 代码任意位置调用exit
  • 调用_exit

2.3 exit函数

在代码的任意地方调用exit,调用后直接终止整个进程

非main函数的return进退出当前函数,进程会继续,非main函数的exit会直接结束进程

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

void Print()
{
    printf("hello world!\n");
    exit(12);
}

int main()
{
    printf("我是一个进程: pid:%d,ppid:%d\n",getpid(), getppid());
    Print();
    exit(123);
}

程序运行进入main函数,打印当前PID和PPID,调用Print()函数,执行Print()函数中的内容,执行到exit(12)时终止整个程序

2.4 _exit函数

_exit函数和exit函数类似,区别在于:exit会强制刷新所有缓冲区 ,_exit直接跳过库层,完全不刷新用户缓冲区

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
	printf("测试1");
	exit(0);  // 刷新缓冲,屏幕输出 测试
	
	printf("测试2");
	_exit(0); // 无换行、无手动fflush时,缓冲区丢失,无输出
}

程序只打印了测试1,说明exit在退出时会刷新缓冲区,而_exit不会

所以,_exit是系统调用,exit是库函数,exit最终会调用_exit,调用_exit前还会执行用户定义的清理函数、冲刷缓冲,关闭流等

通过上面的内容可知,缓冲区和刷新缓冲区的操作一定不在内核中,而是在用户空间中

3、进程等待

3.1 进程等待的重要性

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

3.2 进程等待的方法

3.2.1 wait方法

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>

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

父进程的阻塞式等待:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程执行逻辑
        int cnt = 5;
        while(cnt--)
        {
            printf("我是子进程: pid: %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    else if(id > 0)
    {
        sleep(10);
        pid_t rid = wait(NULL);
        if(rid == id)
        {
            printf("pid: %d, wait success!\n",getpid());
        }
        sleep(5);
    }
    return 0;
}

子进程结束退出时会在一段时间内处于僵尸状态:

一段时间后,父进程执行wait函数,回收僵尸进程,如果成功返回就返回子进程的pid:

3.2.2 waitpid方法

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);
  • 返回值:当正常返回时waitpid返回收集到的子进程的进程ID,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0,如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
  • 参数:pid:pid=-1,等待任一个子进程。与wait等效。
    pid>0,等待其进程ID与pid相等的子进程
  • status:输出型参数,WIFEXITED (status): 若为正常终止子进程返回的状态,则为真,WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码
  • options:默认为0,表示阻塞等待,WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

注意:在status存有子进程的退出信息,如果子进程正常调用exit跑完退出,那么status记录exit(数字)里面的数字,也就是退出码,如果被信号杀死,那么status保存收到的信号

int status 是一个32位的整形图,我们只关注低16位比特位

  • 正常终止

低7位(bit0~bit6):进程正常终止,终止编号为0

bit7:core dump标志,判断是否生成崩溃转存文件

高8位(bit8~bit15):存储exit(xx)里的退出码

  • 被信号杀死

低 7 位(bit0~bit6):终止信号编号 ≠ 0

bit7:core dump 标志

高 8 位(bit8~bit15):无意义,子进程被信号杀死,退出码无效

示例:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    int cnt = 5;
    while(cnt--)
    {
      printf("我是子进程: pid: %d\n", getpid());
      sleep(1);
    }
    exit(1);
  }
  else if(id > 0)
  {
    sleep(10);
    //pid_t rid = wait(NULL);
    int status = 0;
    // 参数1 id:指定要等待回收的子进程PID;参数2 &status:把状态位图写入status变量;参数3:0阻塞
    // 返回值rid:成功返回被回收的子进程PID;失败返回-1
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
      printf("pid: %d, wait success!, status: %d\n",getpid(), status);
    }
    sleep(5);
  }
}

由上图可知,子进程退出码为1,所以status的结构中:高8位:00000001 ,低8位:00000000 ,二进制16位:00000001 00000000 ,换算成16进制:1*2^8=256

利用status表示退出码:(status >> 8)&0xFF

利用status表示退出信号:status&0x7F

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt--)
        {
            printf("我是子进程: pid: %d\n", getpid());
            sleep(1);
        }
        exit(10);
    }
    else if (id > 0)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0);
        if (rid > 0)
        {
            printf("wait success, 退出的子进程: %d, exit_code:%d ,exit sig:%d\n",
                   rid, (status >> 8) & 0xFF, status & 0x7F);
        }
    }
}

上面所呈现的都是阻塞状态下父进程等待子进程,即父进程在调用waitpid时若子进程尚未退出,操做系统检测子进程未退出,父进程就一直阻塞在这里

3.3 非阻塞等待

在阻塞状态下,如果子进程没有处于僵尸状态,那么父进程将一直处于阻塞状态,若父进程采用非阻塞等待,那么子进程没有处于僵尸状态时,父进程就可以干其他事情,在子进程变成僵尸之前,父进程要轮询检测子进程状态

此时非阻塞状态option选项要替换成宏WNOHANG

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt--)
        {
            printf("我是子进程: pid: %d\n", getpid());
            sleep(1);
        }
        exit(10);
    }
    else if (id > 0)
    {
        while (1)
        {
            int status = 0;
            pid_t rid = waitpid(-1, &status, WNOHANG);
            if (rid > 0)
            {
                printf("wait success, 退出的子进程: %d, exit_code:%d\n", rid, WEXITSTATUS(status));
                break;
            }
            else if (rid == 0)
            {
                printf("子进程还在运行,父进程还需要等待!\n");
                usleep(100000);
            }
            else
            {
                perror("waitpid");
                break;
            }
        }
    }
}

在等待过程中父进程可以去执行自己的任务,但也要轮询子进程。父子进程哪个先执行由调度器决定,但退出时确保父进程是最后一个退出的,避免内存泄漏

相关推荐
盐焗鹌鹑蛋3 小时前
【Linux】进程程序替换
linux
鸠摩智首席音效师3 小时前
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead
linux
tianyuanwo3 小时前
DevOps主机管理员视角:企业级Linux主机权限管理策略与实践
linux·服务器·devops
郭涤生3 小时前
Windows + Ubuntu 双系统启动故障修复
linux·windows·ubuntu
三言老师3 小时前
CentOS8 Firewalld 高级规则中级实操教程
linux·运维·服务器·网络·centos
tedcloud1235 小时前
Impeccable 部署指南:开源前端设计工具 Linux 环境搭建实践
linux·运维·服务器·前端·人工智能·开源
风曦Kisaki5 小时前
# Kubernetes(K8s)笔记Day12 :K8s 七层代理(Ingress 和 Ingress Controller)
linux·笔记·云原生·容器·kubernetes
大黄说说5 小时前
SQL Server 执行计划怎么看?快速定位 SQL 慢的根源
java·linux·数据库
yunwei378 小时前
eBPF 教程:精准隔离已建立的 TCP 连接
linux·安全·开源