linux 学习笔记11 多进程——进程的创建

linux 学习笔记11 多进程——进程的创建

进程的创建

Linux下有4类创建子进程的函数,分别为:system,fork。exec,popen。其中,常用的为fork和exec,同时,fork和exec函数族均为系统调用函数。

system

int system(const char *command);

system 函数通过调用 shell 程序/bin/sh –c 来执行 command 所指定的命令,该函数在内部是通过调用
execve(“/bin/sh”,…)函数来实现的。
system函数调用成功返回0
例:system函数调用 " ls -l "命令

#include <func.h>

int main()
{
    system("ls -l");                                                                                          
    return 0;
}

执行结果:
system调用

fork

Linux中fork是一个特别重要的函数:它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。它和其他函数的区别在于:它执行一次返回两个值。其中父进程的返回值是子进程的进程号,而子进程的返回值为 0,若出错则返回-1,因此可以通过返回值来判断是父进程还是子进程。

fork 函数创建子进程的过程为:使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继
承了进程的地址空间,包括进程上下文(当时寄存器的状态)、进程堆栈、内存信息(进程地址空间)、打开的文件描述符(fd)、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制(ulimit)、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数 fork 返回,各自继续往下运行,但是原进程的fork返回值与子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在子进程中,fork返回0,如果fork返回负值,表示创建子进程失败。
以下对父子进程的堆栈空间,文件描述符,进程优先级的区别进行一一分析

Q:为什么父进程一般先于子进程执行
A:涉及fork函数原理:
fork原理
task_struct记录了虚拟地址空间映射了哪一块物理内存
因此 task_struct描述了进程地址空间的存在
task_struct参考:https://blog.csdn.net/qq_36791466/article/details/90212212

fork函数创建父子进程:

#include <func.h>
  //fork基本都是父亲先执行,孩子后执行int main(int argc,char*argv[])
  {
      pid_t pid;
      pid=fork();
      if(0==pid)
      {   
          printf("child Mark1, pid=%d,ppid=%d\n",getpid(),getppid());                                         
      }else
      {   
          printf("dad Mark1.pid=%d,ppid=%d\n",getpid(),getppid());
          sleep(1);//等待子进程结束完
      }   
      return 0;
  }
  

Q:父进程修改或者子进程修改会对对方造成影响吗?(重要)
A:不会,因为子进程独有资源使用,和操作系统的写时复制。
父子进程的栈空间具有区别,父进程栈空间的值(假设为 i=10)的改变,对子进程没有影响,虽然子进程是父进程的复制,父进程的 i 的值改变,子进程的 i 仍为10,此时涉及到写时复制的原理(COW,copy on write)。
写时复制分析如下:
cow1
cow2
cow步骤:
step1:申请空间
step2:内容复制
step3:映射
step4:更改值(如10->5)
cow的优点在于任何一个进程崩溃了,另一个进程不受影响。
代码如下:

 #include <func.h>
  //父子进程的栈空间有何区别int main(int argc,char*argv[])
  {
      pid_t pid;
      pid=fork();
      int i=10;
      if(0==pid)
      {   
          printf("child Mark1,i=%d\n",i);
          printf("&i=%p\n",&i);//&i在父进程改变后,i的地址不会发生变化
          return 0;
      }else
      {   
          printf("dad Mark1.i=%d\n",i);
          printf("&i=%p\n",&i);
          i=5;
          printf("After change i=%d\n",i);//父进程的变量i改变了,对子进程没有影响。
          //涉及到写时复制的原理
          printf("After &i=%p\n",&i);//i的虚拟地址不会发生变化,变化的是物理内存的空间。                       
          sleep(1);
          return 0;
      }   
  }

实现效果如下:
fork_stack

Q:父进程的堆空间修改,是否会对子进程造成影响?
A:不会,原理同为写时复制。
代码如下:

#include <func.h>
  //父子进程的堆空间
  //验证父进程的堆空间进行改变对子进程是否有影响?int main(int argc,char*argv[])                                                                              
  {
      pid_t pid;
      pid=fork();
      char *p=(char*)malloc(20);
      strcpy(p,"hello,mark");
      if(0==pid)
      {//子进程
          printf("child Mark1,%s\n",p);
          return 0;
      }else
      {//父进程
          printf("dad Mark1.%s\n",p);
          strcpy(p,"world");
          printf("after modification,%s\n",p);//子进程不会发生变化,原理同样为写时复制
          sleep(1);
          return 0;
      }   
  }

实现效果如下:
fork_malloc
Q:任一进程崩溃对另一进程是否有影响?
A:没有影响
代码如下:

 #include <func.h>
  //验证任意一个进程崩溃对另一个进程没有影响。int main(int argc,char*argv[])
  {
      pid_t pid;
      pid=fork();
      if(0==pid)
      {   
          printf("child Mark1\n");
          char *p=NULL;
          *p=10;//对空指针赋值,令子进程崩溃                                                                  
          printf("I am Mark2\n");//进程崩溃,本句不会打印
          //ps -elf|grep fork_crash查看子进程状态,此时子进程会成为僵尸(Z)状态
      }else
      {   
          printf("dad Mark1\n");
          while(1);
          sleep(1);
      }   
      return 0;
  }
  

实现效果如下:
fork_crash
此时,父进程3959正在运行,而子进程3960崩溃,成为僵尸进程,但对父进程没有影响。
注意,进程RST可以相互转换,但是Z不行。
Z(僵尸进程):代表进程不能再运行了,但是task_struct还在内核中。

Q:fork父子进程是否指向同一个文件对象?
A:是,文件对象和堆栈不同,父子进程指向相同的文件对象

fork 打开文件描述符原理:
fork_malloc
代码如下:

$ touch file  //创建文件
$ echo -n helloworld>file  //向文件中写入helloworld
#include <func.h>
//验证fork后父子进程指向同一个文件对象
//先open后fork
int main(int argc,char*argv[])
{
    ARGS_CHECK(argc,2);
    pid_t pid;
    int fd=open(argv[1],O_RDWR);
    pid=fork();                                                                                               
    char buf[128]={0};
    if(0==pid)
    {//子进程
        read(fd,buf,5);
        printf("child Mark1,buf is %s\n",buf);
        return 0;
    }else
    {//父进程
        read(fd,buf,5);
        printf("dad Mark1,buf is %s\n",buf);
        sleep(1);
        return 0;
    }   
}

实现效果如下:
fork_openfile
由于父子进程指向同一个文件对象,因此父进程先打印hello,子进程再打印world。

Q:先执行fork函数,再open文件描述符,和先open再fork是否有区别?
A:有区别,先fork再open将会打开两个不一样的文件对象,而先open再fork,父子进程指向同一个文件对象。

代码如下:

#include <func.h>
//验证先fork后open与先open后fork是否有区别
//先fork后open将会产生两个文件对象。
int main(int argc,char*argv[])
{
    ARGS_CHECK(argc,2);
    pid_t pid;
    pid=fork();
    int fd; 
    char buf[128]={0};
    if(0==pid)
    {   
        fd=open(argv[1],O_RDWR);
        read(fd,buf,10);
        printf("child Mark1,buf is %s\n",buf);//子进程指向文件对象1
        close(fd);
        return 0;
    }else
    {   
        fd=open(argv[1],O_RDWR);
        read(fd,buf,10);                                                                                      
        printf("dad Mark1,buf is %s\n",buf);//父进程指向文件对象2
        close(fd);
        sleep(1);
        return 0;
    }   
}

实现效果如下:
先fork后open
指向了不同的文件对象,因此子进程也能打印。

exec函数族

exec函数族包含:
int execl(const char *path, const char arg, … / (char *) NULL */);
int execlp(const char *file, const char arg, … / (char *) NULL */);
int execle(const char *path, const char arg, … /, (char *) NULL, 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[]);

函数族中常用函数:
int execl(const char *path, const char *arg, …);
参数解析:
path 是包括执行文件名的全路径名
arg 是可执行文件的命令行参数,多个用,分割注意最后一个参数必须为 NULL
exec函数是采用第一个参数指定的程序覆盖现有的进程空间。假设已有程序add.c(加法函数),则第一个参数为./add(可执行文件),则代码为:

execl("./add","add","3","4",NULL);
//           argv[0],argv[1],argv[2],NULL代表结尾

execl原理如下:
execl原理
add函数代码如下:

#include <func.h>

int main(int argc,char*argv[])
{
    puts(argv[0]);                                                                                            
    ARGS_CHECK(argc,3);
    int first=atoi(argv[1]);
    int second=atoi(argv[2]);
    printf("sum=%d\n",first+second);
    
    return 0;
}

将add函数编译成为可执行文件add
execl代码如下:

#include <func.h>
  //采用相对路径int main(int argc,char*argv[])                                                                              
  {
      int ret=execl("./add","add","3","4",NULL);
      //execl的原理是覆盖了代码段,成功时execl不会返回,因此以下代码不会执行
      //只有当失败时才会打印
      ERROR_CHECK(ret,-1,"execl");
      printf("Mark Execl  failed\n");
      return 0;
  }

实现效果如下:
execl效果图
其余不常用函数:
int execv(const char *path, char *const argv[]);
execv 功能和 execl 一样,区别如下:

#include <func.h>

 int main(int argc,char *argv[])
  {
     char *args[]={"add","3","5",NULL};//execl和execv的区别在于需要将参数放在指针数组中。                    
      int ret=execv("./add",args);
      ERROR_CHECK(ret,-1,"execv");
      printf("Mark Execl  failed\n");
      return 0;
  }

实现效果如下:
execv
Execle与execl区别在于execle需要先定义一个环境变量envp,利用环境变量执行execl
Test_path代码:

#include <func.h>

int main()
{
    system("echo $PATH");//打印环境变量                                                                       
    return 0;
}

实现效果如下:
test_path
利用test_path 写execle例子
execle代码:

#include <func.h>
  //int execle(const char *path, const char *arg, ...
      //, (char *) NULL, char * const envp[] );
  
 int main(int argc,char*argv[])
  {
     char *const envp[]={"PATH=/usr/bin",NULL};//注意,一定要写NULL
      //如果没有写NULL,执行时没有任何报错,但是环境变量改不了
      int ret=execle("./test_path","test_path",NULL,envp);                                                    
      ERROR_CHECK(ret,-1,"execl");
      printf("Mark Execl  failed\n");
      return 0;
  }

实现效果如下:
execle
execle与execlp区别在于,execlp可以直接用文件名add,不需要用可执行文件./add
同理,execvp与execv区别在于execvp可知直接使用文件名add
execvpe也是使用文件名和环境变量envp执行

popen

popen 函数类似于 system 函数,与 system 的不同之处在于它使用管道工作。
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
command 为可执行文件的全路径和执行参数;
type 可选参数为”r”或”w”
如果为”w”,则 popen 返回的文件流做为新进程的标准输入流(stdin),如果为”r”,则 popen 返回的文件流做为新进程的标准输出流(stdout)。
例子:设printf和scanf的可执行文件作为command

Print.c代码如下:

#include <func.h>
int main()
{
    printf("I am print elf\n");                                                                               
    return 0;
}

Scanf.c代码如下:

#include <stdio.h>                                                                                                              
int main()
{
    int i,j;
    scanf("%d%d",&i,&j);
    printf("%d\n",i+j);
    return 0;
}

标准输入popen的w模式,代码如下:

#include <func.h>
//标准流管道
//
int main()
{
    FILE *fp;
    fp=popen("./scanf","w");
    ERROR_CHECK(fp,NULL,"popen");
    char buf[128]="4 5";
    fwrite(buf,sizeof(char),strlen(buf),fp);
    pclose(fp);                                                                                               
    return 0;
}

实现效果如下:
popen_w
标准输出popen的r模式代码如下:

#include <func.h>
//标准流管道
//
int main()
{
    FILE *fp;
    fp=popen("./print","r");                                                                                  
    //fp=popen("ls -l","r");//for test
    ERROR_CHECK(fp,NULL,"popen");
    char buf[1024]={0};
    fread(buf,sizeof(char),sizeof(buf),fp);
    printf("popen:%s",buf);
    pclose(fp);
    return 0;
}

实现效果如下:

popen_r

补充:
内核暂停一个进程执行时,就会把几个相关处理器寄存器的内容保存在进程描述符中,这些寄存器
包括:
1、 程序计数器(PC)和栈指针(SP)寄存器
2、 通用寄存器
3、 浮点寄存器
4、 包含 CPU 状态信息的处理器控制寄存器(处理器状态字)
5、 用来跟踪进程对 RAM 访问的内存管理寄存器
内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载 CPU 寄存器。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值