Linux系统编程之进程控制

一、进程标识

1.pid

        每个进程都有非负整数表示的唯一进程ID,即pid,其类型为pid_t类型。可用ps命令查看当前所有进程的信息,该命令可以加选项,一般使用ps -ef或ps axf(打印进程树),查看当前系统所有进程的信息。需要注意的是和fd(文件描述符)不同,pid是顺次使用的,即当10001,10002,10003被用过后,10002释放了,下个进程的pid会继续使用10004而不是回头使用最小的可用pid10002。

        pid为1的进程是init进程,它是所有进程的祖先进程。 

 2.getpid(),getppid()

        getpid()用于获取当前进程的进程号,而getppid()用于获取当前进程的父进程的进程号。

         返回值为pid_t类型的进程号。

二、进程的产生

1.fork()

        fork()用于产生一个子进程,这个子进程是从父进程拷贝过来的,因此除了以下这几点其他都是一摸一样的,连产生那一刻执行到的位置都是一样的,即子进程会从创建它的那一行代码开始继续往下执行。

不同点:

        1)返回值不一样

        2)pid和ppid不一样

        3)未决信号和文件锁不一样

        4)资源利用量清零

         如果成功,父进程中的返回值是子进程的pid号,子进程中的返回值是0.如果失败,父进程中的返回值是-1.

        另外,父子进程是写时拷贝的,当他们都是只读的时候他们会共享物理内存页,当谁要修改内容时谁就复制一份自己的新的物理内存页,去修改自己的内容。子进程中的地址是虚拟地址,指针变量的值和父进程一样,但指向的实际物理内存是独立的。

fork使用示例:

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


int main(int argc,char *argv[])
{
    printf("[%d]:Begin\n",getpid());

    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork()");
        exit(1);
    }

    if(pid == 0)
    {
        printf("[%d]:Child is working\n",getpid());
    }
    else
    {
        printf("[%d]:Parent is working\n",getpid());
    }

    printf("[%d]:End\n",getpid());
    exit(0);
}

执行结果:

        父子进程结束顺序是不确定的,由调度器的调度机制决定。

 需要特别注意的是:如果fork之前没有刷新缓冲区,fork之后可能会把之前的内容重复输出,从而导致错误,因此fork之前一定要刷新缓冲区。而且有些内容比如输出到文件的内容是全缓冲的,这时用\n来刷新是无效的,必须要在fork()前用fflush()来刷新缓冲区

将输出重定向到文件中:

 可以看到Begin被输出了两次。

使用fflush():

 

 正确输出。

2.父子进程的关系

        父子进程结束时机不同会产生不同影响。

1)子进程先结束

        当子进程结束时,它的大部分资源会被释放,同时内核向父进程发送SIGCHLD信号。但是,子进程的进程控制块(PCB)仍然存在,里面还存了进程的状态,以便父进程可以通过wait()或waitpid()等系统调用来获取子进程的终止状态。

        如果父进程使用了signal(SIGCHLD,SIG_IGN)来忽略该信号,表示自己对子进程的退出不感兴趣,则系统会立即释放子进程的PCB。

        如果子进程先于父进程结束,且没有忽略信号,那么子进程会成为一个僵尸进程(zombie process)。僵尸进程是指子进程已经终止,但其父进程尚未通过wait()或waitpid()等系统调用来获取子进程的终止状态。在子进程成为僵尸进程后,它的状态会被标记为"Z"(Zombie)或"defunct"。

        父进程可以通过调用wait()或waitpid()等系统调用来等待子进程的终止,并获取其终止状态。当父进程获取到子进程的终止状态后,操作系统会将僵尸进程的PCB从进程表中删除,释放相关资源。

2)父进程先结束

        当父进程先于子进程结束时,子进程会成为一个孤儿进程(orphan process)。孤儿进程是指其父进程已经终止的进程。在这种情况下,子进程的新的父进程会被设置为init进程。

        孤儿进程的状态取决于其具体的执行情况。如果孤儿进程仍在执行,则它将继续运行直到完成。一旦孤儿进程终止,它的资源将被操作系统回收。

三、进程的回收

1.wait(),waitpid()

        wait()和waitpid()都是用来回收子进程的资源的,他们会等待子进程状态变化并获取其状态信息。

         wait()会阻塞等待直到任意一个子进程状态改变,而waitpid可以指定某个子进程,并且可以设置为非阻塞。waitpid()的第一个参数pid和第三个参数options有如下取值,其中options可以用按位或( | )组合使用。

        可以看出,waitpid(-1,&status,0)和wait(&status)是等效的。

        status参数是用来保存子进程状态的,有如下的宏定义。

        二者的返回值差不多,成功返回pid,失败返回-1,不同的是如果waitpid()的options参数设置了WNOHANG(不阻塞),并且调用时没有待回收的子进程,则会返回0.

        如果父进程比较忙,不想让其阻塞等待,可以将wait放到SIGCHLD的信号处理函数中。

四、exec函数簇

        我们知道,在shell下执行的可执行文件其父进程时shell,但fork()只能产生和自身一模一样的进程,而我们的可执行文件并不和shell一样,这是怎么实现的呢?

        其实shell中执行文件产生的进程是通过exec函数簇实现的。exec函数簇可以用新的进程映像(process image)代替旧的进程映像,并在新的进程中从头执行。

        换句话说,exec函数簇会将当前进程替换为新的程序,新程序会从头开始执行。它会重新加载新程序的代码段、数据段和堆栈,并开始执行新程序的入口函数。

        特别注意的是,和fork()一样,也要注意缓冲区的刷新问题,所以调用exec函数簇的函数前要用fflush()刷新缓冲区。

1.execl(),execlp(),execle()

1)execl()接受一个文件路径 path 和一系列的参数,以 NULL 结尾。参数 arg0 是新程序的名称,它会作为 argv[0] 传递给新程序,后面的参数是传递给新程序的命令行参数,最后一个参数必须是 NULL。例如:

execl("/bin/ls", "ls", "-l", NULL);

2)execlp()与execl()类似,但是它会在系统的 PATH 环境变量中搜索指定的可执行文件,并执行找到的第一个匹配的文件。例如:

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

3)execle()与execl()类似,但是它还接受一个环境变量数组 envp,用于设置新程序的环境变量。此 envp 是新进程的所有环境变量,其他未设置的环境变量不会继承。环境变量数组的最后一个元素必须是 NULL。例如:

char *env[] = {"PATH=/usr/bin", "HOME=/home/user", NULL};
execle("/bin/ls", "ls", "-l", NULL, env);

        这些函数如果成功都没有返回值,否则返回-1. 

2.execv(),execvp(),execvpe()

 1)execv()接受一个文件路径 path 和一个参数数组 argv,其中 argv[0] 是新程序的名称,后面的元素是传递给新程序的命令行参数,最后一个元素必须是 NULL。例如:

char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);

2)execvp()与execv()类似,但是它会在系统的 PATH 环境变量中搜索指定的可执行文件,并执行找到的第一个匹配的文件。例如:

char *args[] = {"ls", "-l", NULL};
execvp("ls", args);

3)execvpe()与execvp()类似,但是它还接受一个环境变量数组 envp,用于设置新程序的环境变量。此 envp 是新进程的所有环境变量,其他未设置的环境变量不会继承。环境变量数组的最后一个元素必须是 NULL。例如:

char *args[] = {"ls", "-l", NULL};
char *env[] = {"PATH=/usr/bin", "HOME=/home/user", NULL};
execvpe("ls", args, env);

        这些函数如果成功都没有返回值,否则返回-1. 

 以下是一个使用示例:

int main()
{
    puts("Begin");
    fflush(NULL);
    
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork()");
        exit(1);
    }
    if(pid == 0)
    {
        execl("bin/date","date","+%s",NULL);
        perror("execl()");        //走到这说明execl失败了
        exit(1);
    }

    wait(NULL);

    puts("End");
    exit(0);
}

 运行结果:

五、mysh的实现

        了解了上面的函数,我们就可以自己实现一个简单的shell了,shell可以执行内部命令和外部命令,内部命令我们目前知识还不够,所以当前只需实现可以执行外部命令的shell.

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

#define MAX_ARGC 20

char **parse(char *line)
{
    char **args = (char**)malloc(MAX_ARGC * sizeof(char*));
    char *saveptr;
    char *arg;
    args[0] = strtok(line," ");
    int i = 1;

    while((arg = strtok(NULL," ")) != NULL)
    {
        args[i] = arg;
        i++;
    }

    args[i] = NULL;

    return args;
}

int main(int argc,char **argv)
{
    while(1)
    {
        printf("[user@myshell]");
        char *line = NULL;
        size_t n = 0;
        getline(&line,&n,stdin);
        line[strlen(line)-1] = '\0';        //把\n去掉,否则会影响命令行参数
        char **args = parse(line);

        pid_t pid = fork();
        if(pid < 0)
        {
            perror("fork():");
        }

        if(pid == 0)
        {
            execvp(args[0],args);

            perror("exec:");
            exit(1);
        }
        else
        {
            free(line);
            free(args);
            wait(NULL);
        }
    }

    return 0;
}

        可以执行ls,du,vim等外部命令。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值