【Linux】进程控制

目录

一、进程创建

初识fork函数

在Linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

返回值:在子进程中返回0,父进程返回子进程的PID,子进程创建失败返回-1。

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

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

例子:

运行结果:

这里可以看到三行输出,一行Before,两行After。进程30363先打印Before消息,然后它再打印After。另一个After消息由进程30364打印的。注意到进程30364没有打印Before,为什么呢?
因为Before是由父进程打印的,而调用fork函数之后,则是由父进程和子进程两个进程分别打印After。也就是说,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。
注意: fork之后,父进程和子进程谁先执行完全由调度器决定。

fork函数返回值

子进程返回0,

父进程返回的是子进程的pid。

那么为什么fork有两个返回值?

因为在函数内部准备执行return的时候,我们的主题功能就已经完成了,也就是子进程就已经创建完毕了,那么之后的父进程和子进程都执行了return,所以就返回了两个值。

写时拷贝

在子进程刚刚创建的时候,父子进程的代码是共享的,父子在不写入时,数据也是共享的,只有当任意一方准备写入时,便各自拷贝一份副本,如下图所示:

而这种按需申请资源的策略就是写时拷贝

为什么数据要写时拷贝?

因为进程具有独立性。进程的之间的运行是互不影响的,数据和代码是分开的,代码是共用的,而数据是各自用各自的,不能让一个进程的修改影响到另一个进程,所以就有了写时拷贝,在需要修改数据的时候再分配,这样便可以高效的使用内存空间。

运行结果:

可以看到子进程对全局数据进行修改,由于进程具有独立性,独立性体现在数据层面,在子进程对数据进行修改时,进行了写时拷贝,所以并不影响父进程。

fork常规用法

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

fork调用失败的原因

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

二、进程终止

进程退出场景

进程退出只有三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(进程崩溃)

进程常见退出方法

进程退出都会有一个进程退出码,我们一般以0表示代码正常执行完毕,以非0表示代码执行过程中出现错误,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。

我们看看下面这个代码:

我们可以看到main函数是正常执行完了。

我们也可以通过C语言中的strerror函数打印该错误码在C语言中所对应的错误信息,如下:

_exit函数与exit函数

使用exit函数退出进程也是我们常用的方法,exit函数可以在代码的任意地方调用该函数都表示进程退出,但在调用exit之前,还做了其他工作:

  1. 执行用户通过 atexit 或 on_exit 定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

执行exit(n)等同于执行return n, 因为调用main的运行时函数会将main的返回值当做 exit 的参数。

例如,如下代码中,exit函数终止进程前会将缓冲区当中的数据输出:

但是,_exit函数是直接干掉进程,不会对缓冲区数据进行刷新。

如下:

三、进程等待

进程等待必要性

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

等待的本质:就是通过系统调用获取子进程退出码或者退出信号的方式,顺利释放内存问题。

进程等待的方法

wait

pid_t wait(int* status);

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

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

如下,父进程会等带子进程执行完毕:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		// child
		int count = 3;
		while (count--)
		{
			printf("I am child,PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	// father
	pid_t ret = wait(NULL);
	if (ret > 0)
	{
		// wait success
		printf("wait child success...\n");
	}
	sleep(3);
	return 0;
}

我们先用监控脚本对进程进行实时监控:

我们可以看到子进程退出后,父进程回收了子进程的退出信息,回收了内存空间,子进程也就不会变成僵尸进程了。

waitpid

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: 当设置为WNOHANG时,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

例如:

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		int cnt = 5;
		while (cnt--)
		{
			printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	int status = 0;
	pid_t ret_id = waitpid(id, &status, 0);
	printf("我是父进程,等待子进程成功,pid: %d, ppid: %d\n", getpid(), getppid());

	return 0;
}

注意:

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

获取子进程status

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

我们可以通过为操作,查看根据status得到的进程的退出码和退出信号。

(status >> 8) & 0xFF;//退出码

status & 0x7F;//退出信号

如下:

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		int cnt = 5;
		while (cnt--)
		{
			printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
			sleep(1);
		}
		exit(111);
	}
	int status = 0;
	pid_t ret_id = waitpid(id, &status, 0);
	printf("我是父进程,等待子进程成功,pid: %d, ppid: %d, ret_id: %d, status: %d, child exit code: %d, child exit siginal: %d\n",
		   getpid(), getppid(), ret_id, status, (status >> 8) & 0xFF, status & 0x7F);
	return 0;
}

注意:退出信号为0,则表示代码正常,非0,则表示代码异常。

非阻塞等待测试

父进程一直调用wait/waitpid进行等待,这是阻塞等待。

而可以让父进程不用一直等待子进程退出,而是当子进程未退出时父进程不占用资源,做自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待,那么如何做到呢?

把waitpid 的第三个参数写成 WNOHANG 即可。

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

#define TASK_NUM 10

// 预设一批任务
void sync_disk()
{
    printf("这是一个刷新数据的任务\n");
}

void sync_log()
{
    printf("这是一个同步日志的任务\n");
}

void sync_send()
{
    printf("这是一个进行网络发送的任务\n");
}

typedef void (*func_t)();
func_t other_task[TASK_NUM] = {NULL};

int LoadTask(func_t func)
{
    int i = 0;
    for (; i < TASK_NUM; i++)
    {
        if (other_task[i] == NULL)
            break;
    }
    if (i == TASK_NUM)
        return -1;
    else
        other_task[i] = func;
    return 0;
}

void InitTask()
{
    int i = 0;
    for (i = 0; i < TASK_NUM; i++)
    {
        other_task[i] = NULL;
    }
    LoadTask(sync_disk);
    LoadTask(sync_log);
    LoadTask(sync_send);
}

void RunTask()
{
    int i = 0;
    for (i = 0; i < TASK_NUM; i++)
    {
        if (other_task[i] == NULL)
            continue;
        other_task[i]();
    }
}
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		int cnt = 5;
		while (cnt--)
		{
			printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
			sleep(1);
		}
		exit(111);
	}
	InitTask();
    while (1)
    {
        int status = 0;
        pid_t ret_id = waitpid(id, &status, WNOHANG);
        if (ret_id < 0)
        {
            printf("error\n");
            exit(1);
        }
        else if (ret_id == 0)
        {
            RunTask();
            sleep(1);
            continue;
        }
        else
        {
            if (WIFEXITED(status))
            {
                printf("wait success child exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("wait success child exit siginal: %d\n", status & 0x7F);
            }
		}
	}
	return 0;
}

四、进程程序替换

创建子进程的目的是什么?

1、让子进程执行父进程的一部分代码

2、如果子进程想指向一个全新的程序代码,便有了进程程序替换

替换原理

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

创建进程的时候,OS 先把对应的数据结构内核的PCD空间先创建出来,然后在需要的时候,再通过 execl 把外部的代码录制到内存里、

替换函数

其实有六种以exec开头的函数,统称exec函数:

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

代码演示:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // child
        printf("我是子进程:%d\n", getpid());
        // execl("/bin/ls", "ls", "-a", "-l", NULL);

        char *const myargv[] = {
            "ls",
            "-a",
            "-l",
            "-n",
            NULL};
        execv("/bin/ls", myargv);

        // execlp("ls","ls", "-a", "-l", NULL);

        // char *const myargv[] = {
        //     "ls",
        //     "-a",
        //     "-l",
        //     "-n",
        //     NULL};
        // execvp("ls",myargv);

        exit(1);
    }

    sleep(5);

    // fater
    int status = 0;
    printf("我是父进程\n");
    waitpid(id, &status, 0);
    printf("child exit code: %d\n", WEXITSTATUS(status));
    return 0;
}

函数解释

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

命名理解

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
    下图是exec函数族一个完整的例子:

五、简易的shell

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

代码实现:

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

#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char *commandstr, char *argv[])
{
    assert(commandstr);
    assert(argv);

    argv[0] = strtok(commandstr, SEP);
    if (argv[0] == NULL)
        return -1;
    int i = 1;
    while (argv[i++] = strtok(NULL, SEP))
        ;
    // while (1)
    // {
    //     argv[i] = strtok(NULL, SEP);
    //     if (argv[i] == NULL)
    //         break;
    //     i++;
    // }
    return 0;
}

void debugPrint(char *argv[])
{
    int i = 0;
    for (i = 0; argv[i]; i++)
    {
        printf("%d : %s\n", i, argv[i]);
    }
}

int main()
{
    char commandstr[MAX] = {0};
    char *argv[ARGC] = {NULL};
    while (1)
    {
        printf("[zhangsan@mymachine currpath]#");
        fflush(stdout);
        char *s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s;
        commandstr[strlen(commandstr) - 1] = '\0';

        int n = split(commandstr, argv);
        assert(n == 0);
        if (n != 0)
            continue;
        debugPrint(argv);

        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if (id == 0)
        {
            // child
            execvp(argv[0],argv);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
        // printf("%s\n",commandstr);
    }
    return 0;
}
相关推荐
phoenix09811 小时前
Linux入门DAY29
linux·运维
一休哥助手1 小时前
Naive RAG:简单而高效的检索增强生成架构解析与实践指南
运维·人工智能·架构
叔叔别拉了我害怕2 小时前
封装FTPSClient连接ftps服务器
服务器·git·github
入秋2 小时前
Linux服务器安装部署 Nginx、Redis、PostgreSQL、Docker
linux·前端
不甘懦弱2 小时前
阿里云搭建flask服务器
服务器·python·flask
Bi2 小时前
包含多个子项目集成一个项目部署Vercel方法
运维·前端
Mr. Cao code2 小时前
使用Tomcat Clustering和Redis Session Manager实现Session共享
java·linux·运维·redis·缓存·tomcat
zcz16071278212 小时前
Linux 网络命令大全
linux·运维·网络
the sun342 小时前
Reactor设计模式及其在epoll中的应用
linux·运维·服务器·c++
VVVVWeiYee2 小时前
BGP高级特性
运维·服务器·网络