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函数更加复杂的用法进行学习,下节见喽!

  • 相关阅读:
    Mvc+三层(批量添加、删除、修改)
    js中判断复选款是否选中
    EF的优缺点
    Git tricks: Unstaging files
    Using Git Submodules
    English Learning
    wix xslt for adding node
    The breakpoint will not currently be hit. No symbols have been loaded for this document."
    Use XSLT in wix
    mfc110ud.dll not found
  • 原文地址:https://www.cnblogs.com/webor2006/p/3505855.html
Copyright © 2011-2022 走看看