Linux学习记录——십사 进程控制(1)


1、进程创建

1、fork函数

fork函数从已存在进程中创建一个新进程,新进程为子进程,原进程为父进程。

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

返回值:子进程中返回0,父进程返回子进程id,出错返回-1。fork会有两个返回值,这个上一篇已经写了原因。

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

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

两个进程都没有修改数据的时候,都指向同一块物理内存空间,只有一方尝试修改数据的时候,才会再开一个空间,也就是发生了写时拷贝。

写时拷贝

通常,父子代码共享,父子在不写入时,数据也是共享的,父进程按照自己的模板给子进程创建了虚拟内存,创建了页表,然后指向物理内存中同样的数据,当子进程修改数据时,系统就会拷贝一下子进程的数据,进行修改,并改变页表的映射关系,最后就指向了一个新的物理内存空间。

存在写时拷贝的意义在于,系统不允许不高效的程序出现,父进程中子进程不需要的数据子进程也不会去读取,只有当子进程要用到另外的空间时,写时拷贝才会出现,本质上这是一种资源筛选。

fork常规用法:

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

fork调用失败的原因:

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

2、进程终止

1、情况分类

程序正常退出,可以有结果正确或者不正确地退出。
程序崩溃,本质是进程因为某些原因,收到了操作系统给的信号,比如之前的kill -9。

对于结果是否正确,可以看程序退出码。之前我们会习惯地写,int main() return 0;如果结果正确就返回0,如果不正确,就会返回非0,这个不正确的信息会展现给用户,来判断程序的错误。

mytest:mytest.c
        gcc -o $@ $^
.PHONY:clean
clean:
        rm -f mytest

$@表示目标文件mytest, $ ^表示依赖文件mytest.c

#include <stdio.h>

int add_to_top(int top)
{
    int sum = 0;
    for(int i = 0; i < top; ++i)
    {
        sum += i;
    }
    return sum;
}

int main()
{
    int result = add_to_top(100);
    if(result == 5050) return 0;
    else return 1;
}

一段简短的代码。然后变成可执行程序。因为没写输出,所以我们看不到结果,但是可以通过echo $?命令来查看程序退出码

在这里插入图片描述

但是多次使用后就没有用了,因为这个命令只保留最近一次的退出码。因为上一个echo $?执行成功,所以返回0。

在这里插入图片描述

操作系统对于不同的错误都有对应的退出码,但给到用户的不能是一个数字,而是给错误信息。看一下具体的退出码。

#include <stdio.h>
#include <string.h>    
                                                                                                                                                                                                        
int main()
{
    for(int i = 0; i <= 200; ++i)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    //int result = add_to_top(100);
    //if(result == 5050) return 0;
    //else return 1;
}

在这里插入图片描述
总共提供了133个错误代码。

但并不是退出码和错误信息一定会对应。

2、如何理解进程终止

进程退出时,系统就需要释放对应的内核数据结构 + 代码和数据

3、进程终止的方式

除了main函数return结束,我们也可以用exit函数结束。

在这里插入图片描述

在这里插入图片描述

可以直接退出,exit里面的数字就是退出码。并且即使exit在调用的函数里面,也会直接退出,不再执行下面所有的代码。所以exit在代码的任何地方都可以退出进程。需要加上头文件stdlib.h。

另外一个

在这里插入图片描述

在这里插入图片描述

貌似和exit一样。但从内部来讲,exit是进行完缓冲区的数据,进行完操作后才退出,而_exit是不管不顾,直接找系统干掉这个进程,不等缓冲区刷新。库函数实现的代码里,exit是封装了_exit。

3、进程等待

子进程退出时,如果父进程不去回收,就会变成僵尸进程,会造成内存泄漏。僵尸状态的进程是无法被杀死的。

一个进程的执行是要结果的,子进程结束后用户得需要知道它的状态,代码正常跑完可以通过退出码来知道,运行异常可以通过抛出的信号来知道,所以要想知道子进程执行的结果,就要知道退出码和是否抛出异常。

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

进程等待有两种方式。wait和waitpid。wait会等待所有父进程创建的子进程,如果不传参,传NULL,那么就不管结果,只回收。可以写这样一段代码来展现等待过程。

#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {                         
            printf("我是子进程, 我还活着, 我还有%ds, pid: %d, ppid%d\n", cnt--, getpid(), getppid());
            sleep(1);        
        }
        exit(0);
    }
    //父进程
    pid_t ret_id = wait(NULL);
    printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d\n", getpid(), getppid(), ret_id);
    sleep(5);
    return 0; 
}

子进程退出时,父进程在等待,所以就可以回收子进程。

如果要获取退出结果,就要用到waitpid。

在这里插入图片描述
参数里,pid > 0,表示等待指定的进程;pid = -1,等待任一个子进程,与wait等效。如果等待成功,就会把等待的这个进程的pid返回,失败就返回-1。

第二个参数status是输出型参数,用来获取子进程的退出状态,它的退出状态就是上面3个结果,代码正常结束结果对,结果不对和抛出异常,前两个是退出码,异常是信号,所以这个参数就是来接收信号+退出码的。关于退出时返回的信号,可以用kill -l查看,总共64个信号,前面一半是常用的,不过没有0号信号,所以信号也是数字。这么看来status是要接收2个整数吗?但是实际上我们不应以整数角度看待它,要以位图结构来看待。位图结构简而言之就是看二进制位,status是4个字节,32个比特位。
改一下之前的代码

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

改一下,上面cnt是5。exit括号里是10,但是status的结果并不是10。这时候出现数字的把它写成二进制数,左面16位先不看,后面16位,左面的8个是退出状态,也就是高地址的8位,这里的就是return的或者exit括号里的数字,也就是信号,最低的7个位如果是0,就是正常退出,也就是退出码,对比这两部分就知道结果是否对应上了。最后还剩一位是core dump标志,这个以后再写。那我们获取这两个数。

    //父进程
    int status = 0;
    //pid_t ret_id = wait(NULL);
    pid_t ret_id = waitpid(id, &status, 0);
    printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8) & 0xFF, status & 0x7F);
    sleep(5);

这样code代表结果是否正确,signal代表正常退出。如果在子进程那里有一些异常,程序打印一次就退出了,父进程就回收它了,比如野指针等问题。把while条件改成while(1),也可以用kill -9杀死程序,父进程就回收它了。

父进程是如何获取子进程信息的?一个进程有自己的pcb,地址空间等等,Linux进程的pcb,也就是task_struct结构体,在结构体里有两个变量,对应的就是返回值和退出信号。当进程结束时,系统会把pcb维护起来。waitpid是系统调用接口,能够访问到这个进程的pcb,然后把数据拿到,再返回给用户即可。

子进程在没有退出前,父进程会一直等待子进程死亡,这也就是一种阻塞等待。此时父进程不在运行状态,所以父进程没有运行,它在阻塞队列中待着。等到子进程结束后,pcb中某一个指向父进程的指针会去找父进程,父进程因此用阻塞状态变成运行状态,来到运行队列中,然后调用waitpid回收子进程。

用wait的时候默认是阻塞式调用,而waitpid则是在等待过程中可以让父进程去做其他事,保持运行状态,这是非阻塞轮询。非阻塞时调用时有三个状态,一个正常结束,一个出错,一个正在运行。当第三个参数被设置时,就是非阻塞式调用,如果成功,就返回子进程pid;如果进程存在且正在运行,就返回0;出错返回-1。

改一下代码,变成非阻塞

    //父进程
    int status = 0;
    //pid_t ret_id = wait(NULL);
    pid_t ret_id = waitpid(id, &status, WNOHANG);
    if(ret_id < 0)
    {
        printf("waitpid error!\n");
        exit(1);
    }
    else if(ret_id == 0)
    {
        printf("子进程还没退出,我在做其它事情\n");
        sleep(1);
        continue;
    }
    else
    {
        printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit status: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8) & 0xFF, status & 0x7F);
        break;
    }    
    sleep(5);
    return 0;

为了让父进程做其他事情,还可以有其他办法,先看下现在的代码:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程, 我还活着, 我还有%ds, pid: %d, ppid%d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }
    //父进程
    int status = 0;
    //pid_t ret_id = wait(NULL);
    pid_t ret_id = waitpid(id, &status, WNOHANG);
    if(ret_id < 0)
    {
        printf("waitpid error!\n");
        exit(1);
    }
    else if(ret_id == 0)
    {
        printf("子进程还没退出,我在做其它事情\n");
        sleep(1);
        continue;
    }
    else
    {
        printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit status: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8) & 0xFF, status & 0x7F);
        break;
    }

    sleep(5);
    return 0;
}

人为地放一些任务函数让父进程去调用,并且写一些别的可调用的函数让父进程去做点事

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

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

void net_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()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        other_task[i] = NULL;
    }
    LoadTask(sync_disk);
    LoadTask(sync_log);
    LoadTask(net_send);
}

void RunTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(other_task[i] == NULL) continue;
        other_task[i]();
    }
}

在//父进程之后我们这样写

    //父进程
    while(1)
    {
        int status = 0;
        //pid_t ret_id = wait(NULL);
        pid_t ret_id = waitpid(id, &status, WNOHANG);
        if(ret_id < 0)
        {
            printf("waitpid error!\n");
            exit(1);
        }
        else if(ret_id == 0)
        {
            printf("子进程还没退出,我在做其它事情\n");
            sleep(1);
            continue;
        }
        else
        {
            printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit status: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8) & 0xFF, status & 0x7F);
            break;
        }
    }
    return 0;
}

在这里插入图片描述

之前获取信号和返回码的时候,我们是自己的写的代码,也可以用默认给的宏来获取。

        else
        {
            if(WIFEXITED(status))
            {
                printf("等待成功,子进程退出码: %d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("等待成功,子进程退出码: %d\n", status & 0x7F); 
                //printf("我是父进程, 我等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit status: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8) & 0xFF, status & 0x7F); 
            }
            break;
        }

下一篇继续写进程相关的知识。

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值