Linux APUE学习:7、C程序的进程控制(二) 文件共享 如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的述符是在 fork 之前打开的)。
在fork之后处理文件描述符 有以下两种常见的情况:
父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后 ,它曾进行过读、写操作的任一共享的描述符的文件偏移量已做了相应更新(即子进程终止,子进程共享的文件描述符已经被close过一次)。
父进程和子进程各自执行不同的程序。在这种情况下,在 fork 之后,父进和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
fork一般有以下两种用法:
一个父进程希望复制自己,使得父进程和子进程同时执行不同的代码段 。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
一个进程要执行一个不同的程序 。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
exec*族函数 在前面Linux-APUE学习:3、C程序的启动和终止
时提到,内核执行一个c程序实际上是调用了一个exec函数。
exec
函数族用于执行新的程序,它会将当前进程替换为一个新的程序。当调用 exec
函数族中的任意一个函数时,新程序会完全取代原来的程序,包括进程的代码、数据、堆栈等。
以下是exec
函数族的部分列出:
1 2 3 4 5 6 int execl (const char *path, const char *arg, ...) ;int execlp (const char *file, const char *arg, ...) ;int execle (const char *path, const char *arg, ..., char * const envp[]) ;int execv (const char *path, char *const argv[]) ;int execvp (const char *file, char *const argv[]) ; ... ...
在上面的函数名中,l 表示以列表(list)的形式传递要执行程序的命令行参数,而v表示以数组(vector)的形式传递要执行程序的 命令行参数,而v表示给该命令传递环境变量(environment)。在这么多的函数调用中,我们选择一个实现即可,因为execl()
函数 的参数相对简单些所以使用它要多些。
这里以一个execl()
例程的示例来进行演示:
1 2 3 4 5 6 7 8 9 noah@raspberrypi:~ $ ifconfig wlan0 wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.0.114 netmask 255.255.255.0 broadcast 192.168.0.255 inet6 fe80::5cce:3b12:b977:c50a prefixlen 64 scopeid 0x20<link > ether dc:a6:32:c2:93:41 txqueuelen 1000 (Ethernet) RX packets 869961 bytes 187807095 (179.1 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 510589 bytes 155857354 (148.6 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
我们知道,在Linux下可以使用命令ifconfig wlan0
来获取网卡wlan0的IP地址。其实ifconfig
命令本身是一个程序,这样我们可以在程序里创建一个子进程来执行这个程序即可。如果我们想在C程序代码里获取IP地址,C程序不可能像人眼一样在屏幕上获取IP地址(标准输出默认到屏幕上),这时候就需要在子进程里将标准输出重定向到文件里,这样命令的打印信息会输出到该文件中。之后父进程就可以从该文件中读出相应的内容并作 相应的字符串解析,就可以获取到IP地址了。
下面是该功能程序的实现源码和注释:源码地址
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <ctype.h> #define TMP_FILE "/tmp/.ifconfig.log" int main (int argc, char **argv) { pid_t pid = -1 ; int fd = -1 ; char buf[1024 ]; int rv = -1 ; FILE *fp = NULL ; char *ptr = NULL ; char *ip_start = NULL ; char *ip_end = NULL ; char ipaddr[16 ]; if ((fd = open(TMP_FILE, O_RDWR | O_CREAT | O_TRUNC, 0644 )) < 0 ) { printf ("Redirect standard output to file failure: %s\n" , strerror(errno)); return -1 ; } if ((pid = fork()) < 0 ) { printf ("fork() creat child proess failure: %s\n" , strerror(errno)); return -2 ; } else if (pid == 0 ) { printf ("Child proess start excute ifconfig program\n" ); dup2(fd, STDOUT_FILENO); execl("/sbin/ifconfig" , "ifconfig" , "wlan0" , NULL ); printf ("Child proess excute another program, But now return, it mean is execl() error\n" ); return -3 ; } else { sleep(3 ); } memset (buf, 0 , sizeof (buf)); rv = read(fd, buf, sizeof (buf)); printf ("Read %d bytes date dierectly read after child proess write\n" , rv); memset (buf, 0 , sizeof (buf)); lseek(fd, 0 , SEEK_SET); rv = read(fd, buf, sizeof (buf)); printf ("Read %d bytes date dierectly read after child proess write\n" , rv); memset (buf, 0 , sizeof (buf)); fp = fdopen(fd, "r" ); fseek(fp, 0 , SEEK_SET); while (fgets(buf, sizeof (buf), fp)) { if (strstr (buf, "netmask" )) { ptr = strstr (buf, "inet" ); if (!ptr) { break ; } ptr += strlen ("inet" ); while (isblank(*ptr)) { ptr++; } ip_start = ptr; while (!isblank(*ptr)) { ptr++; } ip_end = ptr; memcpy (ipaddr, ip_start, ip_end - ip_start); break ; } } printf ("Parser and get IP address: %s\n" , ipaddr); fclose(fp); unlink(TMP_FILE); return 0 ; }
运行结果和ifconfig wlan0
对比
vfork()
系统调用 在上面的例子中我们可以看到,在fork()之后常会紧跟着调用exec来执行另外一个程序,而exec会抛弃父进程的文本段、数据 段和堆栈等并加载另外一个程序,所以现在的很多fork()实现并不执行一个父进程数据段、堆和栈的完全副本拷贝。作为替代, 使用了写时复制 (CopyOnWrite)技术: 这些数据区域由父子进程共享,内核将他们的访问权限改成只读,如果父进程和子进程 中的任何一个试图修改这些区域的时候,内核再为修改区域的那块内存制作一个副本 。
vfork()
是另外一个可以用来创建进程的函数,他与fork()
的用法相同,也用于创建一个新进程。 但vfork()
并不将父进程的地址 空间完全复制到子进程中 ,因为子进程会立即调用exec
或exit()
,于是也就不会引用该地址空间了。不过子进程再调用exec()或 exit()之前,他将在父进程的空间中运行 ,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果 ,因为他会影响了父进程空间的数据可能会导致父进程的执行异常。此外,vfork()
会保证子进程先运行,在他调用了exec或exit()之后父进程才 可能被调度运行。如果子进程依赖于父进程的进一步动作,则会导致死锁。
总的来说,vfork()
函数被一些人认为是有瑕疵的函数 ,在SUSv3 中,vfork()
被标记为弃用的接口,在SUSv4 中被究全删除。可移植的应用程序不应该使用这个函数。
vfork()
的函数原型和fork()
原型一样:
1 2 3 4 5 #include <unistd.h> #include <sys/types.h> pid_t fork (void ) ; pid_t vfork (void ) ;