Linux APUE学习:7、C程序的进程控制(二)

文件共享

​ 如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的述符是在 fork 之前打开的)。

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

  1. 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享的描述符的文件偏移量已做了相应更新(即子进程终止,子进程共享的文件描述符已经被close过一次)。
  2. 父进程和子进程各自执行不同的程序。在这种情况下,在 fork 之后,父进和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

​ fork一般有以下两种用法:

  1. 一个父进程希望复制自己,使得父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
  2. 一个进程要执行一个不同的程序。这对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>

// 在内存中的文件系统/tmp
#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];

// 如果忘记了open参数,其为
// (文件描述符, 可读可写|如果不存在则创建|以可写/可读可写打开时将光标置于文件首, 文件权限)
// 打开不成功则返回值<0,不存在 = 0,这里不存在则创建,故不可能,打开成功返回该文件的文件描述符
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");
// 重定向函数,把输出重定向至fd文件描述符对应的文件
dup2(fd, STDOUT_FILENO);

/*******************重点部分**********************/

// 执行文件(程序), 命令, 参数, 最后以NULL结尾
execl("/sbin/ifconfig", "ifconfig", "wlan0", NULL);

// execl并不会返回,执行execl后,该进程将抛弃父进程的文本段、数据段,加载指定参数的程序的文本、数据段并重新建立内存空间
// 若exec*类的函数返回,则说明发生错误
printf("Child proess excute another program, But now return, it mean is execl() error\n");
return -3;
}
else
{
// 父进程等待3s,等待子进程先执行
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)) // 一次读一行,读到文件尾返回NULL
{
// 如果行中包含netmask
if (strstr(buf, "netmask"))
{
ptr = strstr(buf, "inet");
if (!ptr)
{
break;
}
// 跳过字符inet
ptr += strlen("inet");

// inet后是空白符,不清楚是空格还是TAB,用isblank()判断
// 若是空白符则跳过
while (isblank(*ptr))
{
ptr++;
}

// 跳过空白符后是IP地址的起始字符
ip_start = ptr;

// IP地址的点分十进制后是空白符,故只要判断出现空白符后视为IP地址的尾
while (!isblank(*ptr))
{
ptr++;
}

ip_end = ptr;
// 使用memcpy()将ip_start~ip_end两指针中间的内存
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()并不将父进程的地址 空间完全复制到子进程中,因为子进程会立即调用execexit(),于是也就不会引用该地址空间了。不过子进程再调用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);