Unix/Linux编程:fork()进程详解

理论

进程

问: 什么是进程

  • 一个程序的执行称为一个进程,所有的代码都是在进程中执行的。
  • 进程也是操作系统进程资源分片的基本单位

问: 进程是怎么产生的

  • Linux中,除了内核启动进程之外,其他的进程都是由它的父进程产生的。【通过调用fork函数】
    在这里插入图片描述

问:操作系统是怎么识别各个进程的

  • 操心系统通过进程ID(pid)来识别,pid 是进程在操作系统中的唯一标志
  • pid是可以重复使用的,比如说前一个为pid=11的进程死掉了,那么pid=11的这个进程就可以分配给其他进程使用了:当pid达到最大限制时,内核会从头开始查找闲置的pid并使用最先找到的那一个作为新进程的id

操作系统中有一些进程ID是专用的:

  • ID为0的进程叫做调度进程,它是内核的一部分,并不执行任何磁盘上的程序,因此页表成为系统进程
  • ID为1的进程叫做init进程
    • 复制在自举内核之后启动系统,以及读取系统初始化文件,并将系统引导到一个状态。
    • init进程是一个以超级用户权限运行着的普通用户进程,不是系统进程
#include <unistd.h>
/*
* 功能:调用进程的进程ID
*/
__pid_t getpid (void)
/*
* 功能:调用进程的父进程ID
*/
__pid_t getppid (void)
/*
* 功能:调用进程的实际用户ID
*/
__uid_t getuid (void) 
/*
* 功能:调用进程的有效用户ID
*/
__uid_t geteuid (void)
/*
* 功能:调用进程的有效组ID
*/
__gid_t getegid (void)

//上面这些函数都没有出错返回

fork,wait,exec

  • 系统调用 fork()允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆等,但是共享代码段。可将此视为把父进程一分为二,术语 fork 也由此得名。
  • 库函数exit(status)终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核。参数status为一整形变量,标识进程的退出状态。父进程可以通过系统调用wait()来获取该状态

库函数 exit()位于系统调用_exit()之上。这里只是强调,在调用fork()之后,父、子进程中一般只有一个会通过调用 exit()退出,而另一进程则应使用_exit()终止。

  • 系统调用wait(&status)的目的有二:

    • 其一:如果子进程尚未调用exit()终止,那么wait()会挂起父进程直到子进程终止
    • 其二:子进程的终止状态通过 wait()的 status 参数返回
  • 系统调用 execve(pathname,argv,envp)加载一个新程序(路径名为pathname,参数列表为argv,环境变量列表为envp)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行(execing)一个新程序。

下图对 fork()、exit()、wait()以及 exece()之间的相互协同作了总结。(此图勾勒了 shell 执行一条命令所历经的步骤:shell 读取命令,进行各种处理,随之创建子进程以执行该命令,如此循环不已。
在这里插入图片描述

  • 父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存
  • 当有进程写的时候使用了写时拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了
  • fork从父进程返回子进程的pid,从子进程返回0,调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1
  • exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1

fork

SUSv3 将 vfork()标记为已过时,SUSv4 则进一步将其从规范中删除。所以应尽量避免使用vfork()。

#include<unistd.h>
#include<sys/types.h>
pid_t fork( void);

返回值:

  • pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中。
  • pid_t 是一个叫做“文件描述符”的数据结构中的一个属性:
    • 负值:创建子进程失败。
    • 在父进程中,fork返回新创建子进程的进程ID;
    • 在子进程中,fork返回0;

注意:

  • fork函数被调用一次但返回两次
  • 进程描述符除了记录pid之外,还记录了进程的优先级、状态、虚拟地址范围以及各种访问权限等,还用一个很有用的属性叫做ppid,它是当前进程的父进程的id。

问: 父子进程的关系

  • 子进程是父进程的副本,它将获得父进程数据段、堆、栈等资源的副本, 但是会共享代码段,都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
  • 每一个副本都是独立的,子进程对于数据他的副本的修改对于其他进程比如父进程、兄弟进程都是不可见的。如果我们相同进程之间相互感知,必须使用进程间的通信手段来通知。

在这里插入图片描述

问: 父子进程的运行时机

  • 由fork创建的新进程被称为子进程, 子进程和父进程会同时运行[但是谁先运行是不一定的,这取决与内核调度]。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

这种不确定性可能会导致所谓“竞争条件(race condition)”的错误

问:fork函数返回的值为什么在父子进程中不同?

程序代码可以通过fork()的返回值来区分父、子进程。

  • 在父进程中,fork()将返回新创建子进程的进程 ID。鉴于父进程可能需要创建,进而追踪多个子进程(通过 wait()或类似方法),这种安排还是很实用的。
  • 而 fork()在子进程中则返回 0。如有必要,子进程可调用 getpid()以获取自身的进程 ID,调用 getppid()以获取父进程 ID

其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.

问:父进程和子进程之间的区别

  • fork的返回值不同
  • 进程ID不同
  • 父进程ID不同
  • 子进程的tms_utimetms_stimetms_cutimetms_ustime的值设置为0

问: fork失败的主要原因

  • 系统中已经有了太多的进程,触及允许该系统创建的最大进程数这一系统级上限。
  • 进程数量要么超出了系统针对此真实用户(real user ID)在进程数量上所施加的限制(RLIMIT_NPROC)
  • 子进程不继承父进程设置的文件锁
  • 子进程的未处理闹钟被清除
  • 子进程的未处理信号机设置为NULL

当无法创建子进程时,fork()将返回-1。

问: 父进程正常运行,子进程终止时会发生什么?

  • 当一个进程正常/异常终止时,内核就向其父进程发送SIGCHLD信号。
  • 子进程终止是异步事件(可以在父进程运行的任何时候发送),所以这个信号也是异步信号。父进程可以忽略这个信号(默认),也可以提供一个该信号发生时即被调用执行的函数

总结

要理解fork()的诀窍的关键是,要意识到,完成对其调用后将存在两个进程,而且而且进程都会从fork()的返回值继续执行

这两个进程将执行相同的程序文本段,却各自拥有不同的栈段、数据段以及堆段拷贝。刚开始时,子进程的栈段、堆段、数据段时对父进程内存相应各部分的完全复制。执行fork()之后,每个进程均可以修改各自的栈段、堆段和数据段,而不影响另一进程。

实践

验证 fork函数被调用一次但返回两次

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
 
int main(int argc,char *argv[]){
    pid_t pid=fork();
    if ( pid < 0 ) {
        fprintf(stderr,"错误!");
    } else if( pid == 0 ) {
        printf("子进程空间");
        exit(0);
    } else {
        printf("父进程空间,子进程pid为%d",pid);
    }
    // 可以使用wait或waitpid函数等待子进程的结束并获取结束状态
    exit(0);
}

在这里插入图片描述

子进程和父进程之间不共享数据空间

子进程的栈段、数据段、堆段是父进程的拷贝

#include <fcntl.h>
#include <zconf.h>
#include <stdio.h>
int		globvar = 6;		/* 全局变量在数据段 */
char	buf[] = "a write to stdout\n";

int main(void)
{
    int		var;		/*自动变量在栈段 */
    pid_t	pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1){
        printf("write error");
        _exit(0);
    }

    printf("before fork\n");	
    if ((pid = fork()) < 0) {
        printf("fork error");
        _exit(0);
    } else if (pid == 0) {		/* 子进程(子进程运行的代码段从这里开始,这行之前的不执行)*/
        globvar++;				/* 修改全局变量和局部变量 */
        var++;
    } else {
        sleep(2);				/* 父进程:睡觉2s以便子进程先运行 */
    }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);
    _exit(0);
}

在这里插入图片描述

从上面可以看出:

  • 父子进程共享代码段,但是不共享数据段:(全部变量和栈上的局部变量是独立的,在子进程中修改不会影响父进程,在父进程中修改也不会影响子进程)

问: 如果将write改成待缓冲的写入,会输出什么?

问:strlen与sizeof的区别

  • strlen计算不包含null字节的字符串长度, sizeof计算包含null字节的字符串长度
  • strlen需要一次函数调用,sizeof在编译时计算缓冲区长度

父子进程间的文件共享

执行fork()时,子进程会获得父进程所有文件描述符的副本。这些副本的创建方式类似于dup(),这也意味着父、子进程中对应的描述符均指向相同的打开文件句柄。打开文件句柄包含有当前文件偏移量(由 read()、write()和 lseek()修改)以及文件状态标志(由 open()设置,通过 fcntl()的 F_SETFL 操作改变)。一个打开文件的这些属性因之而在父子进程间实现了共享。举例来说,如果子进程更新了文件偏移量,那么这种改变也会影响到父进程中相应的描述符

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <zconf.h>

int
main(int argc, char *argv[])
{
    int fd, flags;
    char tmplate[] = "/tmp/testXXXXXX";

    setbuf(stdout, NULL);                   /* Disable buffering of stdout */

    /* Open a temporary file, set its file offset to some arbitrary value,
       and change the setting of one of the open file status flags. */

    fd = mkstemp(tmplate);
    if (fd == -1){
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }


    printf("File offset before fork(): %lld\n",
           (long long) lseek(fd, 0, SEEK_CUR));

    flags = fcntl(fd, F_GETFL);
    if (flags == -1){
        perror("fcntl F_GETFL");
        exit(EXIT_FAILURE);
    }

    printf("O_APPEND flag before fork() is: %s\n",
           (flags & O_APPEND) ? "on" : "off");

    switch (fork()) {
        case -1:
            perror("fork");
            exit(EXIT_FAILURE);


        case 0:     /* Child: change file offset and status flags */
            if (lseek(fd, 1000, SEEK_SET) == -1){
                perror("lseek  SEEK_SET");
                exit(EXIT_FAILURE);
            }

            flags = fcntl(fd, F_GETFL);         /* Fetch current flags */
            if (flags == -1){
                perror("fcntl --- F_GETFL");
                exit(EXIT_FAILURE);
            }
            flags |= O_APPEND;                  /* Turn O_APPEND on */
            if (fcntl(fd, F_SETFL, flags) == -1){
                perror("fcntl --- F_SETFL");
                exit(EXIT_FAILURE);
            }
            _exit(EXIT_SUCCESS);

        default:    /* Parent: can see file changes made by child */
            if (wait(NULL) == -1){
                perror("wait");
                exit(EXIT_FAILURE);
            }         /* Wait for child exit */
            printf("Child has exited\n");

            printf("File offset in parent: %lld\n",
                   (long long) lseek(fd, 0, SEEK_CUR));

            flags = fcntl(fd, F_GETFL);
            if (flags == -1){
                perror("fcntl F_GETFL ");
                exit(EXIT_FAILURE);
            }
            printf("O_APPEND flag in parent is: %s\n",
                   (flags & O_APPEND) ? "on" : "off");
            exit(EXIT_SUCCESS);
    }
}

如果不需要这种对文件描述符的共享方式,那么在设计应用程序时,应于 fork()调用后注意两点:其一,令父、子进程使用不同的文件描述符;其二,各自立即关闭不再使用的描述符(亦即那些经由其他进程使用的描述符)。

在这里插入图片描述

fork的内存语义

从概念上来说,可以将fork()认做是对父进程程序段、数据段、堆段以及栈段创建拷贝。早期的Unix实现中,此类复制确实是如此:将父进程内存拷贝至交换空间,以此创建新进程映像,而在父进程保持自身内存的同时,将换出映像置为子进程。不过,真要是简单地将父进程虚拟内存页拷贝到新的子进程,那就太浪费了。原因有很多,其中之一是:fork()之后尝尝伴随着exec(),这会用新程序替换进程的代码段,并重新初始化其数据段、堆段和栈段。大部分现代Unix实现中采用两种技术来避免这种浪费:

  • 内核将每一进程的代码段标记为自读,从而使进程无法修改自身代码。这样,父子进程可以共享同一代码段。系统调用fork()在为子进程创建代码段时,其所构建的一系列进程级页表项均指向与父进程相同的物理内存页帧
  • 对于父进程数据段、堆段和栈段中的各页,内核采用写时复制(copy-on-write)技术来处理。最初,内核做了一些设置,令这些段的页表项指向与父进程相同的物理内存页,并将这些页面自身标记为只读。调用 fork()之后,内核会捕获所有父进程或子进程针对这些页面的修改企图,并为将要修改的(about-to-be-modified)页面创建拷贝。系统将新的页面拷贝分配给遭内核捕获的进程,还会对子进程的相应页表项做适当调整。从这一刻起,父、子进程可以分别修改各自的页拷贝,不再相互影响

在这里插入图片描述

同步信号以规避 fork()之后的竞争条件

调用 fork()后,无法确定父、子进程间谁将率先访问 CPU。不应对 fork()之后执行父、子进程的特定顺序做任何假设。若确需保证某一特定执行顺序,则必须采用某种同步技术,比如信号量(semaphore)、文件锁(file lock)以及进程间经由管道(pipe)的消息发送等。接下来我们使用同步信号以规避 fork()之后的竞争条件

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <zconf.h>
#include <time.h>
#include <errno.h>
char *currTime(const char *format)
{
#define BUF_SIZE 1000
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}

#define SYNC_SIG SIGUSR1                /* Synchronization signal */
static void             /* Signal handler - does nothing but return */
handler(int sig)
{
}
int main(int argc, char *argv[])
{
    pid_t childPid;
    sigset_t blockMask, origMask, emptyMask;
    struct sigaction sa;

    setbuf(stdout, NULL);               /* Disable buffering of stdout */

    sigemptyset(&blockMask);
    sigaddset(&blockMask, SYNC_SIG);    /* Block signal */
    if (sigprocmask(SIG_BLOCK, &blockMask, &origMask) == -1){
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }


    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sa.sa_handler = handler;
    if (sigaction(SYNC_SIG, &sa, NULL) == -1){
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    switch (childPid = fork()) {
        case -1:
            perror("fork");
            exit(EXIT_FAILURE);

        case 0: /* Child */

            /* Child does some required action here... */

            printf("[%s %ld] Child started - doing some work\n",
                   currTime("%T"), (long) getpid());
            sleep(2);               /* Simulate time spent doing some work */

            /* And then signals parent that it's done */

            printf("[%s %ld] Child about to signal parent\n", currTime("%T"), (long) getpid());
            if (kill(getppid(), SYNC_SIG) == -1){
                perror("kill");
                exit(EXIT_FAILURE);
            }

            /* Now child can do other things... */

            _exit(EXIT_SUCCESS);

        default: /* Parent */

            /* Parent may do some work here, and then waits for child to
               complete the required action */

            printf("[%s %ld] Parent about to wait for signal\n",
                   currTime("%T"), (long) getpid());
            sigemptyset(&emptyMask);
            if (sigsuspend(&emptyMask) == -1 && errno != EINTR){
                perror("sigsuspend");
                exit(EXIT_FAILURE);
            }

            printf("[%s %ld] Parent got signal\n", currTime("%T"), (long) getpid());

            /* If required, return signal mask to its original state */

            if (sigprocmask(SIG_SETMASK, &origMask, NULL) == -1){
                perror("sigprocmask");
                exit(EXIT_FAILURE);
            }


            /* Parent carries on to do other things... */

            exit(EXIT_SUCCESS);
    }
}

在这里插入图片描述

fork


#include <fcntl.h>
#include <zconf.h>
#include <stdio.h>
#include <sys/wait.h>

int main(void)
{
    pid_t	pid;

    if ((pid = fork()) < 0) {
        printf("fork error");
        _exit(0);

    } else if (pid == 0) {		//first child
        printf(" first child from main fork :  curr pid = %ld, parent pid = %ld\n", (long)getpid(),  (long)getppid());
        if ((pid = fork()) < 0){
            printf("fork error");
            _exit(0);
        }else if (pid > 0){
            printf(" first child from main fork :  curr pid = %ld, parent pid = %ld\n", (long)getpid(),  (long)getppid());
            // sleep(10);
            _exit(0);
        }

        sleep(2);
        printf("second child from first child:  pid = %d, getpid = %ld, parent pid(because first child had exit,  parent changed init proceess, so parent child = 1) = %ld\n",pid, (long)getpid(),  (long)getppid());
        _exit(0);
    }

    printf("  main fork :  curr pid = %ld, pid = %ld\n", (long)getpid(),  (long)pid);
    if  (waitpid(pid, NULL, 0) != pid){ /* wait for first child : waitpid会暂时停止目前进程的执行,直到有信号来到或子进程结束。 */
        printf("waitpid error");
        _exit(0);
    }

    printf("main exit\n");
    _exit(0);
}

在这里插入图片描述

  • 当子进程的父进程exit之后,当前子进程的父进程就变成了init进程
    • 在一个进程终止时,内核会逐个检查所有的活动进行,以判断它是否是正在终止进程的子进程,如果是,就将进程的父进程ID更为为1
      在这里插入图片描述

解决竞态

#include "apue.h"

static void charatatime(char *);

int main(void)
{
    pid_t	pid;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        charatatime("output from child child child child child child child child child\n");
    } else {
        charatatime("output from parent parent parent parent parent parent parent parent parent\n");
    }
    exit(0);
}

static void charatatime(char *str)
{
    char	*ptr;
    int		c;

    setbuf(stdout, NULL);			/* set unbuffered: 设置为无缓冲之后,每次字符输出都会调用一次write */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

在这里插入图片描述

#include "apue.h"

static void charatatime(char *);

int
main(void)
{
    pid_t	pid;

    TELL_WAIT(); // 告知需要等待

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        WAIT_PARENT();		/* 等待父进程结束之后再运行 */
        charatatime("output from child child child child child child child child child\n");
    } else {
        charatatime("output from parent parent parent parent parent parent parent parent parent\n");
        TELL_CHILD(pid);  // 告知子进程已经退出了
    }
    exit(0);
}

static void charatatime(char *str)
{
    char	*ptr;
    int		c;

    setbuf(stdout, NULL);			/* set unbuffered */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

在这里插入图片描述

#include "apue.h"

static void charatatime(char *);

int main(void)
{
    pid_t	pid;

    TELL_WAIT();

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        charatatime("output from child child child child child child child child child\n");
        TELL_PARENT(getppid());
    } else {
        WAIT_CHILD();
        charatatime("output from parent parent parent parent parent parent parent parent parent\n");
        TELL_CHILD(pid);
    }
    exit(0);
}

static void charatatime(char *str)
{
    char	*ptr;
    int		c;

    setbuf(stdout, NULL);			/* set unbuffered */
    for (ptr = str; (c = *ptr++) != 0; )
        putc(c, stdout);
}

在这里插入图片描述

  • 2
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unix/Linux系统编程是指使用C语言编写程序,能够调用操作系统提供的系统调用和库函数来完成系统级任务的程序设计过程。Unix/Linux系统编程的目的是编写高效、可靠、安全、移植性好的应用程序或系统程序。 Unix/Linux系统编程的核心代码包括使用系统调用,文件操作(读写文件、目录操作等),进程控制(fork、exec等),信号处理,网络编程等。 在Unix/Linux中,系统调用是与内核进行通讯的标准方式。程序中使用系统调用来请求内核完成某个任务。例如,open()系统调用用于打开一个文件,并返回文件描述符。read()和write()系统调用用于读写文件。 文件操作是Unix/Linux系统编程中的一个重要部分。文件操作包括打开文件、读写文件、删除文件、重命名文件等操作。另外还有目录操作,如创建目录、删除目录、遍历目录等。 进程控制是Unix/Linux系统编程中最为复杂的部分之一。进程控制包括创建新进程、执行新进程、等待进程结束、发送信号给进程等等。其中最常见的系统调用是fork()和exec()。fork()用于创建新进程,而在创建新进程之后,exec()则用于在新进程中执行新的程序。 信号处理是Unix/Linux系统编程中的一个重要概念。信号是由系统发出的一个异步事件,可以从进程内部或外部发出。进程可以对信号进行相应操作。常见的信号包括SIGINT(Ctrl+C中断信号)、SIGTERM(终止进程信号)和SIGKILL(强制终止进程信号)。 网络编程Unix/Linux系统编程中的另一个重要部分。Unix/Linux提供了许多网络编程API,例如socket()、bind()、listen()和accept()等。使用这些API可以编写服务器端和客户端程序,进行网络通信。 总之,Unix/Linux系统编程涉及到许多重要的概念和操作,涉及到操作系统底层的各种操作。因此,需要开发人员有扎实的C编程能力、熟悉Unix/Linux系统调用和库函数、了解进程控制和信号处理的概念、熟悉网络编程API以及充分了解操作系统内部的机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值