zoukankan      html  css  js  c++  java
  • linux系统编程之进程(二)

    今天继续学习进程相关的东东,上节提到了,当fork()之后,子进程复制了父进程当中的大部分数据,其中对于打开的文件,如果父进程打开了,子进程则不需要打开了,是共享的,所以首先先来研究下共享文件这一块的东东:

    fork之后父子进程共享文件:
    首先先通过其原理图来了解一下什么是父子进程共享文件:
    父进程打开两个文件:
     
    这时通过fork()函数新建了一个子进程,这时它共享父进程的文件,其结构如下:
    说明:对于上图不是很清楚的,可以参考博文:http://www.cnblogs.com/webor2006/p/3498443.html
    通过上面的图对父子进程的文件共享有个大概的认识之后,下面以具体代码来进行阐述,接着上节的代码进行实现:
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <signal.h>
    
    
    #define ERR_EXIT(m) 
        do 
        { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while(0)
    
    int main(int argc, char *argv[])
    {
        signal(SIGCHLD, SIG_IGN);
        printf("before fork pid = %d
    ", getpid());
        int fd;
        fd = open("test.txt", O_WRONLY);//父进程打开一个文件
        if (fd == -1)
            ERR_EXIT("open error");
    
        
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork error");
    
        if (pid > 0)
        {
            printf("this is parent pid=%d childpid=%d
    ", getpid(), pid);
            write(fd, "parent", 6);//父进程往文件中写入内容
            sleep(1);//睡眠是为了避免孤儿进程的产生,保证子进程执行的时候,父进程没有退出
        }
        else if (pid == 0)
        {
            printf("this is child pid=%d parentpid=%d
    ", getpid(), getppid());
            write(fd, "child", 5);//子进程往文件中写入内容
        }
        return 0;
    }

    先创建一个"test.txt",里面是空内容:

    也就是可以说明,子进程是共享父进程打开的文件表项的。

    注意:有时候可能会test.txt的内容输出如下:

    上面这种输出并没有按照我们的预想,可能的原因是跟两个进程的静态问题造成的,这个问题比较复杂,可以这样理解:也就是还没等子进程执行,父进程就已经结束了,这时子进程的文件偏移量会从0开始,所以之前父进程写入了parent,由于它退出来,子进程从0的位置开始写,所以最终输出就如上图所示了,为了保证如我们预期来输出,可以将睡眠时间加长上些,保证子进程执行时,父进程没有退出,如下:

    fork【几乎都用它】与vfork【几乎很少用它】:
    上节中我们知道,fork的拷贝机制是copy on write,图中所说的exec函数,是指加载一个新的程序来执行,这个下面会有介绍到,先大概了解下,如果说没有copy on write机制的话,那父子进程都有自己独立的进程空间,也就是子进程需要完完全全的拷贝父进程的地址空间,而如果子进程中执行exec的话,等于它被一个新的程序替换掉了,它根本不需要拷贝父进程的数据,所以就会造成地址空间的浪费,这时才引入了vfork,也就是vfork之后子进程在执行exec之前,是不会拷贝父进程的地址空间的,不管子进程有没有改写数据,它是一个历史问题(说得有点抽象,下面会以具体代码来一一阐述的)。
    下面就以代码来说明这两者的区别:
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <signal.h>
    
    
    #define ERR_EXIT(m) 
        do 
        { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while(0)
    
    int gval = 100;
    
    int main(int argc, char *argv[])
    {
        signal(SIGCHLD, SIG_IGN);
        printf("before fork pid = %d
    ", getpid());
        
        pid_t pid;
        pid = fork();//这里是用的copy on write机制
        if (pid == -1)
            ERR_EXIT("fork error");
    
        if (pid > 0)
        {
            sleep(1);//它的目的是为了让子进程先对gval进行++操作,以便观察父进程是否会受影响
            printf("this is parent pid=%d childpid=%d gval=%d
    ", getpid(), pid, gval);
            sleep(3);
        }
        else if (pid == 0)
        {
            gval++;//子进程来改写数据
            printf("this is child pid=%d parentpid=%d gval=%d
    ", getpid(), getppid(), gval);
        }
        return 0;
    }

    编译运行:

    其原因也就是由于fork()是采用copy on write的机制,下面用图来解析一下上面的结果:

    下面将其改为vfork来实现:

    编译运行:

    这个输出结果,可以表明,vfork产生子进程,当改写数据时也不会拷贝父进程的空间的,父子是共享一份空间,所以当子进程改写的数据会反映到父进程上。

    另外这段程序中出现了一个“段错误”,这是因为:

    这时,编译再运行,就不会有错误了:

    另外执行exec函数也一样,关于它的使用,之后再来介绍。

    提示:vfork是一个历史问题,了解一下既可,实际中很少用它!

    在演示vfork时,提到“子进程必须立刻执行_exit”,那如果用exit(0)退呢?

    编译运行:

    我们通常会将return 0 与exit(0)划等号,但如果在vfork()中,还是划等号么?

    编译运行:

    那exit与_exit有啥区别呢?下面来探讨下,在探讨之前,先来回顾一下进程的五种终止方式

    下面以一个图来说明exit与_exit的区别:

    区别一:exit是C库中的一个函数;而_exit是系统调用。

    区别二:exit在调用内核之前,做了“调用终止处理程序、清除I/O缓冲”;而_exit是直接操作内核,不会做这两件事。

    下面就以具体代码来说明两者的区别:

    编译运行:

    exit(0)相当于return 0;所以可想将上面return 0换为exit(0)也是一样的能在屏幕上打印出来,那如果换成_exit(0)呢?

    这时编译运行:

    这时就正常显示了:

    另外对于exit来说,它会调用“终止处理程序”,所谓“终止处理程序”,就是指在程序结束的时候会调用的函数代码段,这些代码段,需要我们安装才可以,可以用如下函数:

    其中传递的参数是函数指针。

    下面用具体代码来进行说明:

    编译运行:

    如果换成是_exit()呢?

    编译运行:

    插一句:对于fork函数,有一个问题需进一步阐述一下,以便加深对它的理解:

    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <signal.h>
    
    
    #define ERR_EXIT(m) 
        do 
        { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while(0)
    
    int main(int argc, char *argv[])
    {
        signal(SIGCHLD, SIG_IGN);
        printf("before fork pid = %d
    ", getpid());
        
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork error");
    
        if (pid > 0)
        {
            printf("this is parent pid=%d childpid=%d
    ", getpid(), pid);
            sleep(3);
        }
        else if (pid == 0)
        {
            printf("this is child pid=%d parentpid=%d
    ", getpid(), getppid());
        }
        return 0;
    }

    输出:

    对于上面这段程序,就是上节中学习过的,但是有个问题值得思考一下,为啥fork()之后,不是从"before fork"从main的第一行起输出,而是从fork()之后的代码中去输出,这时因为fork()之后,拷贝了“代码段+数据段+堆栈段+PCB”,也就是两个进程的信息几乎都是一样,而由于堆栈段+PCB几乎是一样的,所以它会维护当前运行的信息,所以每个进程会从fork()之后的代码继续执行,这一点需要理解。

    另外,再看一个跟fork()相关的程序:

    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <signal.h>
    
    #define ERR_EXIT(m) 
        do 
        { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while(0)
    
    int main(int argc, char *argv[])
    {
        fork();
        fork();
        fork();
        printf("ok
    ");
        return 0;
    }

    这段程序会打印多少行呢?

    编译运行:

    这是为什么呢?因为第一个fork()时,会产生两个进程,这时这两个进程都会执行它后面的代码,也就是第二个fork(),这时就有四个进程执行第二个fork()了,同样的,这时四个进程就会执行它下面的代码,也就是第三个fork(),这时就再产生四个进程,总共也就是八个进程了,这个比较不好理解,好好想一下!

    最后,我们来说明一下execve函数,这个在上面介绍vfork()函数时,已经提到过了,它的作用是:替换进程映像,这时对它进行使用说明:

    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <signal.h>
    
    #define ERR_EXIT(m) 
        do 
        { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while(0)
    
    int gval = 100;
    
    int main(int argc, char *argv[])
    {
        signal(SIGCHLD, SIG_IGN);
        printf("before fork pid = %d
    ", getpid());
        
        pid_t pid;
        pid = vfork();
        if (pid == -1)
            ERR_EXIT("fork error");
    
        if (pid > 0)
        {
            printf("this is parent pid=%d childpid=%d gval=%d
    ", getpid(), pid, gval);
        }
        else if (pid == 0)
        {
            char *const args[] = {"ps", NULL};
    
            execve("/bin/ps", args, NULL);//将子进程完全替换成/bin/ps中的ps进程命令,所以这句话之后的代码就不会执行了,因为是完全被替换了
            gval++;
            printf("this is child pid=%d parentpid=%d gval=%d
    ", getpid(), getppid(), gval);
        }
        return 0;
    }

    编译运行:

    下节会对execve函数更加复杂的用法进行学习,下节见喽!

  • 相关阅读:
    org.apache.xerces.dom.ElementNSImpl.setUserData(Ljava/lang/String;Ljava/lang
    case when then 中判断null的方法
    Oracle 傻瓜式数据归档
    Object type TYPE failed to create with error
    导出表结构到Excel 生成代码用
    Intellij 高亮显示与选中字符串相同的内容
    自定义命令杀死 java 进程 alias kjava
    R语言包_dplyr_1
    dplyr包
    在天河二号上对比Julia,Python和R语言
  • 原文地址:https://www.cnblogs.com/webor2006/p/3505855.html
Copyright © 2011-2022 走看看