Linux进程控制

目录

一、进程创建

1、fork——创建一个子进程

2、fork()返回值

3、写时拷贝

二、进程终止

1、退出码

2、进程退出的三种方式

三、进程等待

1、wait

2、waitpid

3、status

4、阻塞等待和非阻塞等待

四、进程程序替换

1、exec系列函数

2、l,p,e,v的功能

3、配合fork()测试exec系列函数

五、简易shell的实现

内建命令

一、进程创建

1、fork——创建一个子进程

fork是一个系统函数,在头文件<unistd.h>中被声明,声明如下:

pid_t fork(void);

fork()通过复制调用进程(称为父进程),创建一个新的进程。这个新的进程(称为子进程)是父进程的精确的复制品,但是要注意:

(1)子进程有自己唯一的PID(process id 进程编号),并且这个PID与任何已经存在的进程组的ID不匹配;

(2)子进程的PPID(parent process id 父进程编号)是父进程的PID;

接下来看一段代码:

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

int main()
{
    pid_t id = fork();

    if (id < 0)
    {
        printf("fork() error!\n");
    }
    else if (id == 0)
    {
        printf("child: pid=%d, ppid=%d.\n", getpid(), getppid());
    }
    else 
    {
        printf("parent: pid=%d, ppid=%d\n", getpid(), getppid());
    }

    return 0;
}

执行结果如下:

$ ./my_process 
parent: pid=29357, ppid=19641
child: pid=29358, ppid=29357.

 这里看到两行输出,但是在我们的认知中,一个程序执行if else语句只会执行其中的一个逻辑,要么error,要么child,要么parent,怎么会执行两个逻辑呢,为什么id一会等于0,一会又大于0?

2、fork()返回值

在一个程序中,如果调用了fork()函数,在程序执行到fork()函数内部时,操作系统会做如下工作:

(1)分配新的内存块和内核数据结构给子进程;

(2)将父进程的部分数据结构内容拷贝至子进程;

(3)将子进程添加到系统的进程列表中;

(4)fork返回;

也就是说,当一个进程调用fork时(称为父进程),就会创建一个和父进程二进制代码相同的子进程,而且它们都运行到相同的地方;

在fork()函数返回值之前,子进程也一定被创建好了,并且执行到和父进程一样的地方——返回fork()函数值。

在父进程中,若子进程创建失败,id = -1;若子进程创建成功,就会给父进程返回子进程的PID(>0),给子进程返回0;

这个时候,系统中就有两个进程,每个进程的id不同,所以执行了不同的if else语句,因此会输出两行。

还有一点要注意的是,创建了子进程后,父子进程谁先被调度,先被执行是不确定的,在上述的代码和结果中,虽然if else语句中child的判断逻辑在parent之前,但是先输出的是parent,说明父进程先被调度执行。

3、写时拷贝

在子进程被创建后,为了节省空间,子进程享用父进程的代码和数据,只有当父进程/子进程要修改自己的数据时,会重新拷贝一份数据供两个进程分别使用,这种方式称为写时拷贝。

二、进程终止

1、退出码

进程是来完成某一件事的,这件事完成得怎么样,是成功了,还是失败了,为什么失败了,我们是需要知道的。进程告诉我们它的工作完成得怎么样,就是靠退出码。举个例子:

#include <stdio.h>

int main()
{
    printf("hello world!\n");
    return 1;
}

在main()函数尾部,有一个return 1;语句,这个就标识了这个进程的退出码是1,当一个进程运行结束之后,可以通过echo $?命令来查看上一个进程的退出码:

$ ./mytest 
hello world!
$ echo $?
1
$ echo $?
0
$ echo $?
0

当我们./mytest执行我们的程序结束后,使用echo指令查看其退出码是1,再次使用echo指令,查看的就是echo进程的退出码,是0;

在计算机中,我们可以通过设置不同的退出码来标识不同的退出状态,一般情况下,0表示成功,非0表示出现了问题,每一个数字都对应一种状态,这种状态可以我们自己自定义,也可以使用系统中的映射关系,C语言头文件<string.h>,有一个函数strerror可以查看退出码对应具体意义:

#include <stdio.h>
#include <string.h>

int main()
{   int i = 0;

    for(i = 0; i < 150; ++i)
    {
        printf("%d: %s\n", i, strerror(i));
    }

    return 0;
}

运行结果如下:

 其中,在134之后,就都是没有定义的退出码了。

2、进程退出的三种方式

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

void exit(int status);

调用两个函数可以使进程退出,其中,status表示的是进程的退出状态,只有后16位是有效的,接下来再讲status,现在先比较 _exit() 与 exit() 两个函数。

执行下面的代码,分别调用两个函数

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

int main()
{
    printf("hello world");
    _exit(0);
    
    // exit(0);

    return 0;
}

会出现上图的结果,是因为调用exit()后,exit()刷新了缓冲区,让缓冲区中的内容显示出来。

实际上exit()函数被调用后主要包括以下一些步骤:

(1)执行用户定义的清理函数;

(2)关闭所有打开的流,所有的缓存数据均被写入;

(3)调用_exit()函数。

第三种退出方式是return退出,main函数的调用函数会接收main函数返回的return值当作status传给exit()函数。

三、进程等待

之前学习的僵尸进程,是因为子进程退出后父进程没有及时回收子进程的退出信息,导致内存泄漏,而进程等待,则可以让父进程获取子进程的退出信息,并释放资源。

1、wait

pid_t wait(int *status);

wait系统调用会暂停调用进程的执行,直到它的子进程的其中一个终止。如果调用成功,返回终止的子进程id,否则,返回-1。

2、waitpid

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

waitpid系统调用会暂停调用进程的执行,直到pid指定的子进程状态改变。

当pid的值:

<-1 时,等待进程组号为pid的绝对值的任意一个子进程;

为-1时,等待任意一个子进程;

为0时,等待进程组号与调用进程相同的任何子进程,也就是任意一个和调用进程在同一个进程组的进程;

>0时,等待进程号为pid的进程。

当option的值:

为0时,没有效果;

WNOHANG,如果子进程没有退出,立刻返回。

若函数调用成功,返回状态改变的子进程的id;如果WNOHANG被指定,并且pid指定的进程存在但状态没有改变,返回0。否则,返回-1.

3、status

如果status传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会在status中填入子进程的退出信息。

但是status虽然是int有32位,但只有后16位是有效的,其中,如果进程正常退出,低八位全0,次低八位表示退出状态,如果进程不是正常退出,则低七位表示终止信号,第八位是core dump标志(这个标值下一篇会讲到),次低八位无意义。

 代码测试如下:

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


int main()
{

    // 测试wait()
    pid_t id = fork();

    if (id < 0)
    {
        perror("fork fail");
        exit(1);
    }
    
    if (id == 0)
    {
        // child
        int cnt = 5;
        while(cnt--)
        {
            printf("I'm child, pid:%d, ppid:%d, cnt=%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        exit(10);
    }

    if (id > 0)
    {
        wait(NULL);
        printf("wait success!\n");
    }
    
    return 0;
}

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

int main()
{
    // 测试waitpid()和status
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork fail");
        exit(1);
    }

    if (id == 0)
    {
        // child
        int cnt = 5;
        while(cnt--)
        {
            printf("I'm child, pid:%d, ppid:%d, cnt=%d\n", getpid(), getppid(), cnt);
            sleep(1);
            if (cnt == 1)
            {
                // 这里空指针访问会异常退出
                int *p = NULL;
                *p = 10;
            }
        }
        exit(10);
     }
    if (id > 0)
    {
        // parent
        int status = 0;
        int ret = waitpid(id, &status, 0);
        if (ret > 0 && (status & 0x7F) == 0)
        {
            // 正常退出
            // 退出码应为10(上面设置的)
            printf("exit code: %d\n", (status >> 8) & 0xFF);
        }
        else if (ret > 0)
        {
            // 异常退出
            int sig_code = status & 0x7F;
            printf("signal code: %d\n", sig_code);
            printf("%s\n", strerror(sig_code));
            
        }
    }

    return 0;
}

4、阻塞等待和非阻塞等待

在waitpid()函数的第三个参数option中,可以选择输入WNOHANG,表示如果子进程没有退出,函数会直接返回,意味着父进程可以不用一直等待子进程,在得知子进程没有退出后,父进程可以先去做其他工作,这叫做非阻塞等待,相反,如果父进程一直等到子进程退出才继续其他工作,这叫阻塞等待。

上面的代码测试的都是阻塞等待,接下来对非阻塞等待进行测试。为了方便,先学习几个宏,来对status进行分析,而不用自己再使用移位、按位与等操作。

WIFEXITED(status):如果子进程正常终止(通过exit,_exit,或者来自main()的return)返回真。

WEXITSTATUS(status):返回子进程的退出码,这个宏应该仅在WIFEXITED返回真的情况下使用。

WIFSIGNALED(status):如果子进程被一个信号终止,返回真。

WTERMSIG(status):返回造成子进程终止的信号序号,这个宏应该仅在WIFSIGNALED返回真的情况下使用。

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

int main()
{
    // 测试非阻塞等待
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork fail");
        exit(1);
    }

    if (id == 0)
    {
        // child
        int cnt = 5;
        while(cnt--)
        {
            printf("I'm child, pid:%d, ppid:%d, cnt=%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        exit(10);
     }
    if (id > 0)
    {
        // parent
        int status = 0;
        while (1)
        {
            int ret = waitpid(id, &status, WNOHANG);
            if (ret == 0)
            {
                printf("子进程未退出,于是父进程去做其他事了...\n");
                sleep(1);
            }
            else if (WIFEXITED(status))
            {
                // 正常退出
                // 退出码应为10(上面设置的)
                printf("子进程正常退出, exit code: %d\n", WEXITSTATUS(status));
                exit(0);
            }
            else if (WIFSIGNALED(status))
            {
                // 异常退出
                int sig_code = WTERMSIG(status);
                printf("子进程异常退出, signal code: %d\n", sig_code);
                printf("%s\n", strerror(sig_code));
                exit(0);
            }
        }
    }
    return 0;
}

四、进程程序替换

1、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[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

exec系列函数的作用是用另一个程序来替换当前正在执行的程序,那其中第一个execl来举例,其中第一个参数表示的是要执行程序的路径名,第二个参数表示要执行程序带的选项,其中的...表示可变参数列表,表示执行一个程序可以带多个选项,最后一个选项以NULL结束,例如,我们要执行"ls -a -l --color=auto",其中ls这个程序再user/bin/目录下。

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

int main()
{
    printf("hello world!\n");
    
    execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);

    printf("hello world!\n");

    return 0;
}

从运行结果可以看出,程序确实执行了ls命令,但是两条printf语句只执行了一句!

这就是进程程序替换,替换成功之后,原进程的代码和数据都会被替换,执行新的程序。

2、l,p,e,v的功能

接下来来了解一下另外几个exec系列函数。

函数名带 l,表示传入执行程序的选项时用的时可变参数列表,一个一个传参;

相对的,函数名带 v,表示传入执行程序的选项时用的是一个数组,数组中每一个元素都是选项,最后一个依然需要以NULL结束;

函数名带 p,表示第一个参数不用再传入路径,直接传入要执行程序的名称即可,系统会根据环境变量自动寻找该程序。

函数名带 e,表示需要自己传入环境变量参数,不带e默认继承父进程的环境变量。

3、配合fork()测试exec系列函数

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

extern char **environ;
int main()
{
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("First:\n");
        // 子进程
        execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
        exit(1);
    }
    // 不关心子进程的退出状态
    waitpid(id, NULL, 0);

    id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("\nSecond:\n");
        // 省略路径,直接写程序的名字
        execlp("pwd", "pwd", NULL);
        exit(1);
    }
    waitpid(id, NULL, 0);

    id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("\nThird:\n");
        // 尝试执行当前路径下的自己的程序, environ是系统提供的环境变量
        execle("./mytest", "mytest", NULL, environ);
        exit(1);
    }
    waitpid(id, NULL, 0);

    id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("\nFourth:\n");
        // 函数名带v,选项参数就用数组传递
        char *const my_argv[] = { "ls", "-a", "--color=auto", NULL };
        execv("/usr/bin/ls", my_argv);
        exit(1);
    }
    waitpid(id, NULL, 0);

    id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("\nFifth:\n");
        char *const my_argv[] = { "cat", "mytest.c", NULL };
        execvp("cat", my_argv);
        perror("wrong:");
        exit(1);
    }
    waitpid(id, NULL, 0);

    id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        printf("\nSixth:\n");
        char *const my_env[] = { (char*)"MYENV=11223344", NULL };
        char *const my_argv[] = { (char*)"mytest", NULL };
        execvpe("./mytest", my_argv, my_env);
        exit(1);
    }
    waitpid(id, NULL, 0);

    return 0;
}

五、简易shell的实现

一个shell,首先需要有串提示符,显示当前用户和主机名、当前路径,因此,我们先通过getenv()来获取环境变量,输出提示信息。

第二,输入命令,我们需要读取命令,并把命令存放到command_line这个数组中。

第三,分解命令,将读入的命令分解成一个一个选项。

第四,创建子进程,让子进程调用exec系列函数来执行读入的命令。

(其中cd命令是一个内建命令需要特殊处理,我们最后再解释,先看一下代码和效果吧!)

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

#define NUM_COMMAND 128
#define NUM_OPTION 32

char command_line[NUM_COMMAND] = { 0 };
char *option[NUM_OPTION] = { 0 };
int main()
{
    while (1)
    {
        printf("[%s@%s 当前路径] $ ", getenv("USER"), getenv("HOSTNAME"));
        fflush(stdout);
        // 读取命令行输入到command_line中
        fgets(command_line, NUM_COMMAND - 1, stdin);
        // 读入的换行符改成'\0'
        command_line[strlen(command_line) - 1] = 0;

        // 将读入的命令分割开来,如"ls -a -l"->"ls" "-a" "-l" NULL
        option[0] = strtok(command_line, " ");
        int i = 1;
        while ((option[i++] = strtok(NULL, " ")) != NULL);
        
        // 内建(内置)命令 cd 
        if (option[0] != NULL && strcmp(option[0], "cd") == 0)
        {
            if (option[1] != NULL)
            {
                chdir(option[1]);
            }
            continue;
        }

        // 让子进程来执行命令
        pid_t id = fork();
        assert(id >= 0);
        
        if (id == 0)
        {
            execvp(option[0], option);
            exit(1);
        }
        waitpid(id, NULL, 0);
    }
    return 0;
}

内建命令

在谈论上述的cd内建命令之前,我们先来看一下下面的运行结果:

我们让左边的程序一直输出自己的pid,通过右边查看进程列表,可以看到该进程下有两个路径,其中exe表示的是当前进程对应的是磁盘中哪个文件。cwd表示的是当前进程的工作路径。

shell也是一个进程,使用cd命令后,改变的路径也是shell的工作路径(如果改变的是shell对应在磁盘中的位置,那cd命令岂不是可以更改文件位置?)。

假如shell让子进程来执行cd命令,子进程的工作目录当然会改变,但是父进程也就是shell的工作目录却没有改变,因此如果注释掉内建命令那一段代码,cd命令就会无法正常执行。

类似cd这样的需要父进程自己来执行的内建命令还有很多,就不在此一一列举了,毕竟这只是一个简易shell的实现,大家有兴趣可以自己来尝试完成呀。

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王红花x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值