fork函数

来源:互联网 发布:淘宝拿货网 编辑:程序博客网 时间:2024/06/06 02:21

comment:以前以为在fork之前的printf,子进程是不copy的,读了这篇文章才知道原因。在fork之前的printf在子进程中不执行,只将存在缓冲区。

因为输出到屏幕。只能行缓冲。所以在fork之后的printf,将原来缓冲区的内容冲掉了。fork之后的所有的printf每句都执行。

同比:输出到文件,是全缓冲,不会冲掉。当有printf的时候,将缓冲区的东东全输出.

8.3 fork函数

        一个现有进程可以调用fork函数创建一个新进程。

?
1
2
3
4
       #include <unistd.h>
 
       pid_t fork(void);
        // 返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1

        由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

        子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间。父、子进程共享正文段。

        由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任何一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟内存中的一“页”。

        《UNIX环境高级编程》P173:程序清单8-1 fork函数示例

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int     glob = 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)
        // buf长度减1,是为了避免将终止符null字节写出。
        fprintf(stderr, "write error\n");
    printf("before fork\n");            // 未刷新标准输出缓冲区
 
    if ((pid = fork()) < 0) {
        fprintf(stderr, "fork error\n");
    else if (pid == 0) {              // 子进程
        glob++;                         // 修改变量
        var++;
    else {
        sleep(2);                       // 父进程
    }
 
    printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
 
    exit(0);
}

        如果执行此程序则得到:

?
1
2
3
4
5
6
7
8
9
10
11
12
$ ./01                交换方式运行
a write to stdout
before fork
pid = 3100, glob = 7, var = 89          子进程的变量值改变了
pid = 3099, glob = 6, var = 88          父进程的变量值没有改变
$ ./01 > temp.out        将输出重定向到文件
cat temp.out 
a write to stdout        不带缓冲,只输出一次
before fork        第一次输出
pid = 3102, glob = 7, var = 89
before fork        第二次输出
pid = 3101, glob = 6, var = 88

        从中可以看到,子进程对变量所作的改变并不影响父进程中该变量的值。

        一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。在上述程序中,父进程使自己休眠2秒,以使子进程先执行。但并不保证2秒已经足够。

        strlen和sizeof之间的差别:使用strlen需进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行了初始化,其长度是固定的,所以sizeof在编译时计算缓冲区长度。

        write函数是不带缓冲的,但是,标准I/O库是带缓冲的。如果标准输出连接到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出行两次,其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。于是,那时父、子进程各自有了带该行内容的标准I/O缓冲区。在exit之前的第二个printf将其数据添加到现有的缓冲区中。当每个进程终止时,最终会冲洗其缓冲区中的副本。

文件共享

        对程序清单8-1需注意的另一点是:在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父、子进程的每个相同的打开描述符共享一个文件表项。

        如果一个进程具有三个不同的打开文件,它们是标准输入、标准输出和标准出错。在从fork返回时,有了如下所示的结构:

                        

        这种共享文件的方式使父、子进程对同一文件使用同一个文件偏移量。如果父、子进程都向标准输出进行写操作,并且父进程的标准输出已重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。

        如果父、子进程写到同一描述符文件,但又没有任何形式的同步,那么它们的输出就会相互混合。

        在fork之后处理文件描述符有两种常见的情况:

        (1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行读、写操作的任一共享描述符的文件偏移量已执行了相应更新。

        (2) 父、子进程各自执行不同的程序段。在这种情况下,在fork后,父、子进程各自关闭它们不需要再使用的文件描述符,这样就不会干扰对方使用的文件描述符。

        除了打开文件之外,父进程的很多其他属性也有子进程继承,包括:(1) 实际用户ID、实际组ID、有效用户ID、有效组ID。(2) 附加组ID。(3) 进程组ID。(4) 会话ID。(5) 控制终端。(6) 设置用户ID标志和设置组ID标志。(7)当前工作目录。(8) 根目录。(9) 文件模式创建屏蔽字。(10) 信号屏蔽和安排。(11) 针对任一打开文件描述符的在执行时关闭标志。(12) 环境。(13) 连接的共享存储段。(14)存储映射。(15) 资源限制。

        父、子进程之间的区别是:(1) fork的返回值。(2) 进程ID不同。(3) 两个进程具有不同的父进程ID。(4) 子进程的tms_utime、tms_stime、tms_cutime以及tms_ustime均被设置为0。(5) 父进程设置的文件锁不会被子进程继承。(6) 子进程的未处理闹钟(alar)被清除。(7) 子进程的未处理信号集设置为空集。

        fork失败的两个主要原因时:系统中已经有太多的进程,或者该实际用户ID的进程总数超过了系统限制。

        fork有下面两种用法:

        (1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的:父进程等待客户端的服务请求。当请求到来时,父进程调用fork,使子进程处理此请求,父进程则继续等待下一个服务请求到达。

        (2) 一个进程要执行不同的代码段。在这种情况下,子进程从fork返回后立即调用exec。


http://my.oschina.net/lowkey2046/blog/296868


0 0
原创粉丝点击