Linux C 进程控制

进程创建


fork

#include <unistd.h>

 pid_t fork(void);
  • 函数功能:创建一个子进程,子进程拷贝父进程的数据段,代码段,父子进程各占一份虚拟地址空间
  • 返回值(fork()函数有两个返回值):

    • 若>0:父进程fork()返回子进程进程ID
    • 若=0:子进程fork()返回0
    • 若<0:fork()创建子进程失败
  • 进程创建子进程失败的原因:

    • 内存不够
    • 进程数量太多,达到系统上限
  • 可以通过ulimit -u 命令查看当前系统可允许创建进程数的上限

[root@localhost lcong]# ulimit -u
3780
  • Q:为什么子进程返回值为0,父进程返回值为子进程ID
  • A:1.我们猜想的子进程返回值可能为:父进程ID or 子进程ID。首先,子进程要想获得父进程可以根据getppid()函数获得,所以不需要子进程返回;其次,子进程ID不可能是0,因为进程ID为0的进程是系统进程。
  • 2.我们猜想的父进程返回值可能为:父进程ID or 子进程ID。首先,父进程要想获得自己的ID,可以通过调用getppid()函数,所以不需要返回;其次,一个父进程可以创建多个子进程,但是没有一个函数可以一次性获取所有子进程的ID,所以父进程每fork()一次,返回当前创建的子进程ID
  • 进程创建的实例
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    pid_t pid;
    pid = fork();
    if(pid<0)
    {
        perror("fork");
        return -1;
    }else if(pid == 0)
    {
        printf("i am  child:%d pid=%d\n ",getpid(),pid);
    }else
    {
        printf("i am father:%d pid=%d\n",getppid(),pid);
    }

    return 0;
}
getpid():获得子进程进程ID
getppid():获得父进程进程ID
  • 运行结果
i am father:95260 pid=115718
i am  child:115718 pid=0
  • fork()函数的执行逻辑

    • 父进程创建一个子进程,子进程以父进程为模板(子进程的PCB从父进程拷贝过来)
    • 父子进程共用一份代码,但各有一份数据(写时拷贝)
    • 子进程继承了父进程的PC指针,子进程从fork()返回的位置继续执行
    • fork之后,父子进程执行顺序取决于系统的调度算法
  • 程序实例(验证父子进程各有一份虚拟空间(子进程复制父进程的虚拟地址空间))

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

int main(void)
{
    int var=6;
    pid_t pid;
    pid=fork();
    if(pid<0)
    {
        perror("fork");
        return -1;
    }else if(pid==0)
    {
        var++;
        printf("i am child:%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
    }else
    {
        var--;
        printf("i am father:%d pid=%d var=%d(%p)\n",getppid(),pid,var,&var);
    }

    return 0;
}
  • 运行结果
i am father:95260 pid=116567 var=5(0x7ffddd4375b8)
i am child:116567 pid=0 var=7(0x7ffddd4375b8)
  • 从程序运行结果来看,父子进程对对方的数据操作并没有什么影响,这说明父子进程的数据是各自一在一份虚拟地址空间的
  • 写时拷贝

    • 内核只为新生成的子进程创建虚拟地址空间结构,他们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,他们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间
  • fork的常规用法

    • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码。例如在网络编程中,父进程等待客户端的请求,fork()出子进程来处理请求
    • 一个进程要执行一个不同的程序。例如,子进程从fork()返回后,调用exec函数进行进程程序替换(模拟实现myshell简易版,后面会提到实现思想和代码实例)

vfork

#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);
  • 函数功能:创建一个子进程,子进程和父进程共享地址空间
  • 返回值:同fork()
  • vfork()创建进程实例
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    pid_t pid;
    pid=vfork();
    if(pid < 0)
    {
        perror("vfork");
        return -1;
    }else if(pid == 0)
    {
        printf("i am child:%d pid=%d\n",getpid(),pid);
    }else
    {
        printf("i am father:%d pid=%d\n",getppid(),pid);
    }
    return 0;
}
  • 程序执行结果
i am child:117305 pid=0
i am father:95260 pid=117305
i am child:117306 pid=0
段错误
  • 提示出现了段错误,并且上述程序中我们并没有使用循环结构,但是子进程信息打印了两次;经查阅资料,vfork()只有在调用exit或者exec父进程才可能被调度运行,如果子进程没有调用,程序则会导致死锁,程序是有问题的,没有意义。
  • 程序实例(子进程共享父进程地址空间)
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int var=10;
    pid_t pid;
    pid=vfork();
    if(pid < 0)
    {
        perror("vfork");
        return -1;
    }else if(pid == 0)
    {
        var++;
        printf("i am child:%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
        exit(0);
    }else
    {
        var--;
        printf("i am father:%d pid=%d var=%d(%p)\n",getppid(),pid,var,&var);
    }
    return 0;
}
  • 运行结果
i am child:118408 pid=0 var=11(0x7ffd7925da88)
i am father:95260 pid=118408 var=10(0x7ffd7925da88)
  • 通过程序运行结果我们可以观察到,父子进程相互之间对数据的操作是有影响的,说明他们操作的是同一个虚拟地址空间中的同一个数据,这是因为子进程是在父进程的地址空间中运行的
  • vfork()和fork的用法大致相同,有两个不同点:
    • vfork()出来的子进程一定比父进程先执行,直到子进程调用了exec或者_exit,父进程才会继续执行
    • vfork()出来的父子进程共用一份虚拟地址空间(代码和数据都相同),当然也共用一份内存。在子进程调用exec或者_exit之前,在父进程的空间中运行,会改变父进程的PCB

总结:内核是如何为父子进程分配空间的

父进程f1其虚拟地址空间上有代码段、数据段、堆和栈四个区域,内核会为其分配相应的物理内存

  • fork():f1创建子进程f2,为其复制代码段、数据段、堆和栈四部分;为其分配物理内存:f2的代码段->f1代码段的物理地址;f2的数据段->f2自己的数据段块;f2的堆和栈->f2自己的堆栈块
  • 写时拷贝:内核只为新生成的子进程创建虚拟地址空间结构,他们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,他们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间
  • vfork():内核不为子进程创建虚拟内存空间,直接与父进程共享同一块虚拟内存空间,相应的也共享同一块物理内存

进程等待


进程等待的作用

  • 父进程通过进程等待,拿到子进程的执行结果(执行码)
  • 避免僵尸进程
  • 进程等待能够保证子进程先终止,父进程后终止

wait

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
  • 函数执行逻辑
    • 若子进程还没有终止,父进程会阻塞到wait()函数中,直到子进程终止,父进程才从wait()中返回
    • 调用wait()的时候,若子进程已经退出(处于僵尸态),wait()就会立刻返回,父进程拿到子进程的退出信息,并且释放子进程的相关资源
    • 若调用wait的时候,并没有子进程,wait返回-1,调用失败
  • 参数
    • status:输出参数,由操作系统填充
    • status不能简单当做整型来看待,应该当做一个位图来理解
      • status & 0x7f,若这个值为0,表示代码执行完了,正常退出;若不为0,表示进程异常退出,具体的值表示等待进程退出的信号的编号
      • (status>>8)&0xff 表示子进程退出码,如果进程异常退出,此部分内容没有意义
      • coredump
    • 如果父进程不想知道子进程的退出信息,直接传一个NULL
  • 返回值:等到的子进程的进程ID

  • 具体代码实现:

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

int main(void)
{
    pid_t pid;
    pid=fork();

    if(pid==-1)
    {
        perror("fork");
        return -1;
    }
    if(pid == 0)
    {
        sleep(20);
        exit(10);
    }else
    {
        int status;
        int ret = wait(&status); //父进程阻塞到wait直到子进程退出后继续
        if(ret>0 && (status&0x7f) == 0)//正常退出
        {
            printf("child exit code is:%d\n",(status>>8)&0xff);
        }
        else if(ret>0) //异常退出
        {
            printf("sig code is:%d\n",status&0x7f);
        }
    }
}
  • 程序执行结果
[root@localhost fork]# ./a.out //等待20秒退出(正常退出)
child exit code is:10

[root@localhost fork]# ./a.out //在其他终端kill调(异常退出)
sig code is:9

waitpid

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
  • 执行逻辑
    • 阻塞式等待:
      • 行为和wait类似
    • 非阻塞时等待:
      • 如果要等待的子进程已经执行完了,那么waitpid就返回子进程的pid,同时释放子进程对应的资源
      • 如果我等待的子进程还没有执行完,waitpid会立刻返回,同时返回值是0
  • 参数

    • pid:只等待指定pid的子进程,如果pid值传了-1,表示等待所有子进程
    • status:同wait
    • options:WNOHANG(一旦设置,则以非阻塞等待)
  • 进程的阻塞等待方式代码实现

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

int main(void)
{
    pid_t pid;
    pid = fork();

    if(pid == -1)
    {
        perror("fork");
        return -1;
    }else if(pid == 0)
    {
        printf("child(%d) is runninng...\n",getpid());
        sleep(10);
        exit(10);
    }else
    {
        int status = 0;
        int ret = waitpid(-1,&status,0);
        printf("this is test for wait\n");
        if(WIFEXITED(status) && ret == pid)
        {
            printf("wait child 10s success,child return code is:%d\n",WEXITSTATUS(status));
        }else
        {
            printf("wait child faild..\n");
            return -1;
        }
    }
    return 0;
}
  • 程序运行结果
[root@localhost fork]# ./a.out
child(120243) is runninng...
this is test for wait
wait child 10s success,child return code is:10
  • 进程的非阻塞等待方式代码实现
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

int main(void)
{
    pid_t pid;
    pid = fork();

    if(pid == -1)
    {
        perror("fork");
        return -1;
    }else if(pid == 0)
    {
        printf("child(%d) is running...\n",getpid());
        sleep(5);
        exit(1);
    }else
    {
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(-1,&status,WNOHANG);
            if(ret == 0)
            {
            printf("child is running..\n");
            }
            sleep(1);
        }while(ret == 0);

        if(WIFEXITED(status) && ret == pid)
        {
            printf("wait child 10s success,child return code is:%d\n",WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed..\n");
            return -1;
        }
    }
}
  • 程序运行结果
[root@localhost fork]# ./a.out
child is running..
child(120602) is running...
wait child failed..

[root@localhost fork]# ./a.out
child is running..
child(120936) is running...
child is running..
child is running..
child is running..
child is running..
child is running..
wait child 5s success,child return code is:1

进程终止


  • exit函数
#include <stdlib.h>

void exit(int status);
  • _exit函数
#include <unistd.h>

void _exit(int status);
  • 说明:status定义了进程的终止状态,父进程通过wait来获取该值。虽然status是int,但是只有低八位可以被父进程所用,所以_exit(-1)时,在终端执行$?返回值是255

进程终止的情况

  • 代码执行完了,程序结果正确
    • main() :return 0
    • _exit(0):系统调用
    • exit():(库函数)
      • 执行用户atexit/on_exit定义的清理函数
      • 关闭流,刷新缓冲区
      • 调用_exit()
  • 代码执行完了,程序结果不正确
    • main():return 非0
    • _exit(非0)
    • exit(非0)
  • 代码异常终止
    • kill
    • 代码写出了bug(内存越界)
    • ctrl C

通过在shell下查看进程退出码

  • echo $?

进程程序替换


基本原理

  • 并没有创建一个新进程(没有创建一个新的PCB)
  • 代码段,数据段替换成可执行文件对应的代码和数据
  • 堆和栈都要重新开始
  • 要替换的可执行程序的main函数

相关函数

#include <unistd.h>

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

命名理解:
l(list):表示参数采用列表

v(vector):参数用数组

p(path):有p自动搜索环境变量PATH

e(env):表示自己维护环境变量
  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • exec函数族如果执行成功,没有返回值
  • 如果调用出错返回-1

  • 通过进程替换函数,我们可以实现以个简单的shell


举例说明:

当我们在终端敲下命令”ls -a”时,shell从用户读取到这个字符串,建立一个新的进程,然后在这个进程中运行”ls”程序并等待这个进程结束

所以我们写shell需要循环以下过程:

  1. 获取命令行(通过fgets()函数将命令读取到字符数组buf[]中)

  2. 解析命令行(命令行可分为命令部分和参数部分,我们将buf[]数组中读取到的字符串进行切分,命令存放在commd[]数组中,参数放在para[]中):命令和参数之间是由空格进行分隔的,一次来进行切分,考虑到我们现在实现的是一个简易版的shell,所以我们只解析类似于“ls -a”这种类型的命令,更多的内容我会在后续的博客中进行实现

  3. 建立一个子进程,子进程进行程序替换(执行execlp函数:将其传入的命令替换到进程中的程序部分,这样execlp执行的就是我们给execlp()传递的命令和参数了),父进程等待子进程退出


    • 实现代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

#define MAX 1024

void Parse(char *buf,char *cmmd,char *para)
{
    int c_count=0;
    int p_count=0;
    int i=0;

    //printf("buf:%s\n",buf);

    if(buf[strlen(buf)-1]=='\n')
        buf[strlen(buf)-1]='\0';

    memset(cmmd,0,MAX);
    memset(para,0,MAX);

    while(buf[i]==' ')
        i++;

    while(buf[i]!=' ')
    {
        cmmd[c_count]=buf[i];
        i++;
        c_count++;
    }

    while(buf[i]==' ')
        i++;

    while(buf[i]!='\0')
    {
        if(buf[i]!=' ')
        {
            para[p_count]=buf[i];
            p_count++;
        }
        i++;
    }

    para[i]='\0';
//  printf("cmmd:%s para:%s\n",cmmd,para);
}

void Execute(char *buf,char *cmmd,char *para)
{
    int status;
    pid_t pid;
    pid=fork();
    if(pid<0)
    {
        perror("fork");
        exit(1);
    }else if(pid==0)
    {
        if(para[0]!=0)
        {
            execlp(cmmd,cmmd,para,NULL);
        }
        execlp(cmmd,cmmd,NULL);
        perror("execlp");
        exit(2);
    }else
    {
        pid=waitpid(pid,&status,0);
        if(pid<0)
        {
            perror("waitpid");
            exit(3);
        } 
    }
    memset(buf,0,MAX);
    printf("[lcong@myshell]# ");
}

int main(int argc,char* argv[])
{
    char buf[MAX];
    char cmmd[MAX];
    char para[MAX];

    printf("[lcong@myshell]# ");
    while((fgets(buf,MAX,stdin))!=NULL)
    {
        Parse(buf,cmmd,para);
        Execute(buf,cmmd,para);
    }
    return 0;
}
  • 执行结果:
[root@localhost fork]# ./myshell
[lcong@myshell]# mkdir ll
[lcong@myshell]# ls -a
.  ..  01.c  02.c  03.c  04.c  05.c  06.c  a.out  ll  llROR;JSLOG  myshell  myshell.c
[lcong@myshell]# ls -l
total 52
-rw-r--r--. 1 root root  335 Apr 12 07:38 01.c
-rw-r--r--. 1 root root  414 Apr 12 09:32 02.c
-rw-r--r--. 1 root root  437 Apr 12 10:47 03.c
-rw-r--r--. 1 root root  580 Apr 12 11:59 04.c
-rw-r--r--. 1 root root  735 Apr 12 12:28 05.c
-rw-r--r--. 1 root root  877 Apr 12 12:55 06.c
-rwxr-xr-x. 1 root root 9032 Apr 14 00:40 a.out
drwxr-xr-x. 2 root root    6 Apr 14 00:40 ll
drwxr-xr-x. 2 root root    6 Apr 14 00:20 llROR;JSLOG
-rwxr-xr-x. 1 root root 9032 Apr 14 00:40 myshell
-rw-r--r--. 1 root root 1612 Apr 14 00:39 myshell.c
[lcong@myshell]# pwd
/home/lcong/linux/fork
[lcong@myshell]# cat myshell.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

#define MAX 1024

void Parse(char *buf,char *cmmd,char *para)
{

本人能力有限,只能实现简单的功能,随着学习知识的增多,我会进一步对此版本简易shell不断改进。如果博客内容有错误,希望大佬指出,不吝赐教!

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/79936330