使用C语言的fork()函数在Linux中创建进程的实例讲解

时间:2021-11-15 16:50:24

在Linux中创建一个新进程的唯一方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,和以往遇到的函数有一些区别,因为fork()函数看起来执行一次却返回两个值。

fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而园进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的上下文、代码段、进程堆栈、内存信息、打开的文件描述符、符号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。

因为子进程几乎是父进程的完全复制,所以父子两进程会运行同一个程序。这就需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程不可能做不同的事。实际上是在父进程中执行fork()函数时,父进程会复制一个子进程,而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行,从而使两个进程分别获得所属fork()函数的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0。因此,可以通过返回值来判断该进程的父进程还是子进程。

同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork()函数的系统开销比较大,而且执行速度也不是很快。

fork()函数语法

使用C语言的fork()函数在Linux中创建进程的实例讲解

fork()函数出错可能有两种原因:
1、当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
2、系统内存不足,这时errno的值被设置为ENOMEM

示例

下面的是csapp.h头文件,后面的讨论中均只用该头文件来完成程序的编写。 

?
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/* $begin csapp.h */
#ifndef __CSAPP_H__
#define __CSAPP_H__
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
/* Default file permissions are DEF_MODE & ~DEF_UMASK */
/* $begin createmasks */
#define DEF_MODE  S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
/* $end createmasks */
 
/* Simplifies calls to bind(), connect(), and accept() */
/* $begin sockaddrdef */
typedef struct sockaddr SA;
/* $end sockaddrdef */
 
/* Persistent state for the robust I/O (Rio) package */
/* $begin rio_t */
#define RIO_BUFSIZE 8192
typedef struct {
  int rio_fd;        /* Descriptor for this internal buf */
  int rio_cnt;        /* Unread bytes in internal buf */
  char *rio_bufptr;     /* Next unread byte in internal buf */
  char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;
/* $end rio_t */
 
/* External variables */
extern int h_errno;  /* Defined by BIND for DNS errors */
extern char **environ; /* Defined by libc */
 
/* Misc constants */
#define  MAXLINE   8192 /* Max text line length */
#define MAXBUF  8192 /* Max I/O buffer size */
#define LISTENQ 1024 /* Second argument to listen() */
 
/* Our own error-handling functions */
void unix_error(char *msg);
void posix_error(int code, char *msg);
void dns_error(char *msg);
void app_error(char *msg);
 
/* Process control wrappers */
pid_t Fork(void);
void Execve(const char *filename, char *const argv[], char *const envp[]);
pid_t Wait(int *status);
pid_t Waitpid(pid_t pid, int *iptr, int options);
void Kill(pid_t pid, int signum);
unsigned int Sleep(unsigned int secs);
void Pause(void);
unsigned int Alarm(unsigned int seconds);
void Setpgid(pid_t pid, pid_t pgid);
pid_t Getpgrp();
 
/* Signal wrappers */
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
void Sigdelset(sigset_t *set, int signum);
int Sigismember(const sigset_t *set, int signum);
 
/* Unix I/O wrappers */
int Open(const char *pathname, int flags, mode_t mode);
ssize_t Read(int fd, void *buf, size_t count);
ssize_t Write(int fd, const void *buf, size_t count);
off_t Lseek(int fildes, off_t offset, int whence);
void Close(int fd);
int Select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
    struct timeval *timeout);
int Dup2(int fd1, int fd2);
void Stat(const char *filename, struct stat *buf);
void Fstat(int fd, struct stat *buf) ;
 
/* Memory mapping wrappers */
void *Mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
void Munmap(void *start, size_t length);
 
/* Standard I/O wrappers */
void Fclose(FILE *fp);
FILE *Fdopen(int fd, const char *type);
char *Fgets(char *ptr, int n, FILE *stream);
FILE *Fopen(const char *filename, const char *mode);
void Fputs(const char *ptr, FILE *stream);
size_t Fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
void Fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
 
/* Dynamic storage allocation wrappers */
void *Malloc(size_t size);
void *Realloc(void *ptr, size_t size);
void *Calloc(size_t nmemb, size_t size);
void Free(void *ptr);
 
/* Sockets interface wrappers */
int Socket(int domain, int type, int protocol);
void Setsockopt(int s, int level, int optname, const void *optval, int optlen);
void Bind(int sockfd, struct sockaddr *my_addr, int addrlen);
void Listen(int s, int backlog);
int Accept(int s, struct sockaddr *addr, socklen_t *addrlen);
void Connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
 
/* DNS wrappers */
struct hostent *Gethostbyname(const char *name);
struct hostent *Gethostbyaddr(const char *addr, int len, int type);
 
/* Pthreads thread control wrappers */
void Pthread_create(pthread_t *tidp, pthread_attr_t *attrp,
      void * (*routine)(void *), void *argp);
void Pthread_join(pthread_t tid, void **thread_return);
void Pthread_cancel(pthread_t tid);
void Pthread_detach(pthread_t tid);
void Pthread_exit(void *retval);
pthread_t Pthread_self(void);
void Pthread_once(pthread_once_t *once_control, void (*init_function)());
 
/* POSIX semaphore wrappers */
void Sem_init(sem_t *sem, int pshared, unsigned int value);
void P(sem_t *sem);
void V(sem_t *sem);
 
/* Rio (Robust I/O) package */
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
void rio_readinitb(rio_t *rp, int fd);
ssize_t  rio_readnb(rio_t *rp, void *usrbuf, size_t n);
ssize_t  rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
 
/* Wrappers for Rio package */
ssize_t Rio_readn(int fd, void *usrbuf, size_t n);
void Rio_writen(int fd, void *usrbuf, size_t n);
void Rio_readinitb(rio_t *rp, int fd);
ssize_t Rio_readnb(rio_t *rp, void *usrbuf, size_t n);
ssize_t Rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
 
/* Client/server helper functions */
int open_clientfd(char *hostname, int portno);
int open_listenfd(int portno);
 
/* Wrappers for client/server helper functions */
int Open_clientfd(char *hostname, int port);
int Open_listenfd(int port);
 
#endif /* __CSAPP_H__ */
/* $end csapp.h */

fork()函数示例一

?
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "csapp.h"
int main()
{
 pid_t pid;
 int x=1;
 pid=fork();
 if(pid==0) {
  printf("child :x=%d\n",++x);
  exit(0);
 }
 printf("parent:x=%d\n",--x);
 exit(0);
}

例如上面的程序,由于fork()函数比较特殊,执行一次,返回两次。返回两次分别是在父进程和子进程中各返回一个值,在子进程中返回为0,在父进程中返回进程ID,一般为正整数即非零。这样就能根据返回值来确定其在哪个进程中了。如上面的程序,子进程中pid=0,所以执行if语句,子进程会共享父进程的文本/数据/bss段/堆以及用户栈,子进程随后正常终止并且返回码为0,因此子进程不执行后续的共享代码块,因此本程序的输出结果是

parent:x=0
child :x=2

fork()函数示例二

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "csapp.h"
 
int main()
{
 if(fork()==0) {
  printf("a");
 }
 else {
  printf("b");
  waitpid(-1,NULL,0);
 }
 printf("c");
 exit(0);
}

此程序是用来检验子进程与父进程的关系。同样再次强调一遍,fork()函数用于新建子进程,子进程具有与父进程相同的用户级虚拟地址空间,包括文本/数据/bss段/堆/用户栈,子进程可以读写任意父进程打开的文件,它们的最大区别是它们有不同的PID。fork函数调用一次,返回两次,一次在父进程中,其返回子进程的PID;在子进程中,fork返回0,因为子进程的PID总是非零的,返回值就提供了一个明确的方法来辨别是在父进程中执行还是在子进程中执行。waitpid()函数是等待子进程终止,若无错误,则返回值为正数。因为在在子进程中,fork()返回0,因此先输出a,并且其共享父进程的代码段,故又输出c;而在父进程中,fork()返回值非零,所以执行else语句,故输出bc。因此本程序的输出结果为
acbc

fork()函数示例三

?
1
2
3
4
5
6
7
8
9
#include "csapp.h"
int main()
{
 int x=1;
 if(fork()==0)
  printf("printf1:x=%d\n",++x);
 printf("printf2:x=%d\n",--x);
 exit(0);
}

本程序再次演示子进程与父进程的区别。程序中,在子进程中,子进程共享数据x=1,并且fork()返回0,因此if语句被执行,输出printf1:x=2,接着共享后面一部分代码段,因此再输出printf2:x=1;而对于父进程,fork()返回非零,因此不会执行if语句段,而执行后面的代码,即输出printf2:x=0.因此本程序输出结果为(子进程与父进程顺序不唯一)

?
1
2
3
printf2:x=0
printf1:x=2
printf2:x=1

fork()函数示例四

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "csapp.h"
#define N 3
int main()
{
 int status,i;
 pid_t pid;
 
 for(i=0;i<N;i++)
  if((pid=fork())==0) //新建子进程
   exit(100+i);
 
 while((pid=waitpid(-1,&status,0))>0) { //如果子进程是正常终止的,就返回进程的进程号PID
  if(WIFEXITED(status)) //返回退出状态
   printf("child %d terminated normally with exit status =%d\n",pid,WEXITSTATUS(status));
  else
   printf("child %d erminated abnormally\n",pid);
 }
 
 if(errno!=ECHILD)
  printf("waitpid error\n");
 exit(0);
}

本代码主要是测试进程的终止,即waitpid案例程序。定义生成两个进程,本例子是不按照特定顺序来回收僵死子进程,本程序返回结果为

?
1
2
3
child 28693 terminated normally with exit status =100
child 28694 terminated normally with exit status =101
child 28695 terminated normally with exit status =102

fork()函数示例五

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "csapp.h"
#define N 2
int main()
{
 int status,i;
 pid_t pid[N],retpid;
 
 for(i=0;i<N;i++)
  if((pid[i]=fork())==0)
   exit(100+i);//退出并返回状态码
 i=0;
 while((retpid=waitpid(pid[i++],&status,0))>0) {
  if(WIFEXITED(status))
   printf("child %d terminated normally with exit status=%d\n",retpid,WEXITSTATUS(status));
  else
   printf("child %d terminated abnormally\n",retpid);
 }
 
 if(errno !=ECHILD)
  printf("waitpid error!");
 exit(0);
}

按照创建进程的顺序来回收这些僵死进程,注意程序中的pid[i++]是按序的标志,本程序运行结果为

?
1
2
child 29846 terminated normally with exit status=100
child 29847 terminated normally with exit status=101

fork()函数示例六

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "csapp.h"
 
/*推测此程序会输出什么样的结果*/
 
int main()
{
 int status;
 pid_t pid;
 
 printf("Hello\n");
 pid=fork();
 printf("%d\n",!pid);
 if(pid!=0) {
  if(waitpid(-1,&status,0)>0) {
   if(WIFEXITED(status)!=0)
  printf("%d\n",WEXITSTATUS(status));
  }
 }
 printf("Bye\n");
 exit(2);
}

首先父进程会输出Hello,子进程新建成功,在子进程中,pid为0,输出1,并且子进程无法执行if语句,但是子进程仍然可以输出Bye,并且正常退出并返回状态码为2。而在父进程中,pid为非零的正数,因此先输出0,然后执行if语句,由于子进程已经正常退出,故输出状态码2,并且最后执行公共代码块,输出Bye并正常退出。因此,总共输出的结果如下所示:

?
1
2
3
4
5
6
Hello
0
1
Bye
2
Bye

当然,顺序不唯一,还有一种可能的结果是

?
1
2
3
4
5
6
Hello
1
Bye
0
2
Bye