6 min read

Categories

Tags

In computing, particularly in the context of the Unix operating system and its workalikes, fork is an operation whereby a process creates a copy of itself. It is usually a system call, implemented in the kernel. Fork is the primary (and historically, only) method of process creation on Unix-like operating systems.

fork() creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process.

FreeBSD source code

1
2
3
4
5
6
7
8
Function fork(td, uap)
{
    ...
     error = fork1(td, RFFDG | RFPROC, 0, &p2);

     Return pid of child
     td->td_retval[0] = p2->p_pid;
}


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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
Function fork1(td, flags, pages, procp)
{
     struct proc *p1, *p2, *pptr; //p1 = parent process, p2 = child process

     Parent process
     p1 = td->td_proc;

     ...
     ...
     Allocate a new process
     newproc = uma_zalloc(proc_zone, M_WAITOK);

     To run a process we need to have atleast one thread,
     so allocate a new thread
     td2 = thread_alloc();

     Linkup thread to process
     proc_linkup(newproc, td2);
     ...
     ...

     Now we have to gro through the process address space and copy each section for new process
     Sections of process address space are
     1. Text area (read only)
     2. Initialized area
     3. Uninitiaized area
     4. Stack/Heap area

     vm2 = vmspace_fork(p1->p_vmspace);

          Function vmspace_fork(p1->p_vmspace) {
                    Process virtual address space is as follows
                    /*
                    * struct vmspace {
                    *      struct vm_map vm_map;     /* VM address map */
                    *      struct shmmap_state *vm_shm;     /* SYS5 shared memory private data XXX */
                    *      segsz_t vm_swrss;     /* resident set size before last swap */
                    *      segsz_t vm_tsize;     /* text size (pages) XXX */
                    *      segsz_t vm_dsize;     /* data size (pages) XXX */
                    *      segsz_t vm_ssize;     /* stack size (pages) */
                    *      caddr_t vm_taddr;     /* (c) user virtual address of text */
                    *      caddr_t vm_daddr;     /* (c) user virtual address of data */
                    *      caddr_t vm_maxsaddr;     /* user VA at max stack growth */
                    *      int     vm_refcnt;     /* number of references */
                    *      /*
                    *       * Keep the PMAP last, so that CPU-specific variations of that
                    *       * structure on a single architecture don't result in offset
                    *       * variations of the machine-independent fields in the vmspace.
                    *       */
                    *      struct pmap vm_pmap;     /* private physical map */
                    * };
                    */

               We allocate the process address space
               struct vmspace *vm2;
               ...
               vm2 = vmspace_alloc(old_map->min_offset, old_map->max_offset);
               ...
               ...

               Now we have to go through each vm map entries one at a time and allocate each for new process
               while (old_entry != &old_map->header) {
                    case VM_INHERIT_NONE:
                         ...

                    case VM_INHERIT_SHARE:
                         ...

                    case VM_INHERIT_COPY:
                         This is our case where we have to clone the entries and link into the map
                         new_entry = vm_map_entry_create(new_map);
                         ..
                         vm_map_entry_link(new_map, new_map->header.prev, new_entry);
                         vmspace_map_entry_forked(vm1, vm2, new_entry);
                         vm_map_copy_entry(old_map, new_map, old_entry,
               }
          } // end vmspace_fork()


     We will be now locking the process tree as we need to give new process process id
     Process of allocating a new Pid is system will scan the existing pids and try to come
     up with a range of free pids. Once a range of free pids found system will use that range
     for next lets say "k" forks. Once the range is exhausted then system on next fork will
     again find the range and so on.

     sx_slock(&proctree_lock);

     Determine if we are exceeding process createlimit
     Check privledge to increase the limit

     //Try to allocate the pids
     trypid = lastpid + 1;

     ...

     Check if we have hit the end of the range
     if (trypid >= pidchecked) {
          We have exhausted the range so find a new range
          Scan the active and Zombie procs to check whether pid is in use
          ...
          ...
     }
     sx_sunlock(&proctree_lock);

     If we are within range then
     lastpid = trypid;

     p2 = newproc;
     p2->p_state = PRS_NEW;          /* protect against others */
     p2->p_pid = trypid;


     sched_fork(td, td2);
               Function sched_fork(struct thread *td, struct thread *child) {
                    sched_fork_thread(td, child);
                              Function sched_fork_thread(td, child) {
                                   Initialize schedule related values from parent to child
                                   Assign CPU estimate info and priority
                                   ...
                              }
               } // end sched_fork()

     Insert new process to process list
     LIST_INSERT_HEAD(&allproc, p2, p_list);

     Insert into PID HASH
     LIST_INSERT_HEAD(PIDHASH(p2->p_pid), p2, p_hash);


     Lock P1
     Lock P2
     Relase Lock for All proc since we took all proc lock to allocate new pid
     sx_xunlock(&allproc_lock);

     copy the blocks
     bcopy(&p1->p_startcopy, &p2->p_startcopy,
         __rangeof(struct proc, p_startcopy, p_endcopy));

     Zero out the blocks
     bzero(&p2->p_startzero,
         __rangeof(struct proc, p_startzero, p_endzero));

     ...
     Allocate new signals

     Copy File descriptors of all the open files by parent to child
     fd = fdcopy(p1->p_fd);


     In a new process, we created a thread now copy the blocks for this new thread
     bzero(&td2->td_startzero,
         __rangeof(struct thread, td_startzero, td_endzero));

     bcopy(&td->td_startcopy, &td2->td_startcopy,
         __rangeof(struct thread, td_startcopy, td_endcopy));

     bcopy(&p2->p_comm, &td2->td_name, sizeof(td2->td_name));

     ...
     ...

     Copy the vnode pointer of text from parent to child
     p2->p_textvp = p1->p_textvp;   // Vnode of executable. and increase the ref count
     vref(p2->p_textvp);

     ...


     Processing related to process group, child and parent will belong to same process group
     Insert process into process group list


     Associate child with Parent
     pptr = p1;
     p2->p_pptr = pptr;

     Insert child process into sibling list
     LIST_INSERT_HEAD(&pptr->p_children, p2, p_sibling);

     vm_forkproc(td, p2, td2, vm2, flags);
               Function vm_forkproc(td, p2, td2, vm2, flags) {
                    ...
                    ...

                    cpu_fork(td, p2, td2, flags);
                              Function cpu_fork(struct thread *td1, struct proc *p2, struct thread *td2, int flags)
                              {
                                   Copy PCB and the stack
                                   pcb1 = td1->td_pcb;

                                   Allocate PCB for child process
                                   pcb2 = (struct pcb *)(td2->td_kstack +
                                   td2->td_kstack_pages * PAGE_SIZE) - 1;
                                   td2->td_pcb = pcb2;

                                   Copy PCB1 to PCB2 (PCB struct is in pcb.h)
                                   bcopy(td1->td_pcb, pcb2, sizeof(*pcb2));

                                   Create a new stack for a new process
                                   td2->td_frame = (struct trapframe *)((caddr_t)td2->td_pcb - 16) - 1;
                                   bcopy(td1->td_frame, td2->td_frame, sizeof(struct trapframe));
                                   ...
                                   td2->td_frame->tf_eax = 0;          /* Child returns zero */
                                   ...
                                   ...

                                   In instruction pointer of child process we will set the address of trampoline code
                                   pcb2->pcb_eip = (int)fork_trampoline;

                                   //Fork_trampoline code will call fork_return
                                   pcb2->pcb_esi = (int)fork_return;     /* fork_trampoline argument */
                                   pcb2->pcb_ebx = (int)td2;          /* fork_trampoline argument */

                                             Looking into fork_trampoline in i386/exception.s
                                                  ENTRY(fork_trampoline)
                                                            pushl     %esp               /* trapframe pointer */
                                                            pushl     %ebx               /* arg1 */
                                                            pushl     %esi               /* function */
                                                            call     fork_exit
                                                  So, from fork_trampoline we are going to call fork_exit and its argument
                                                  will be above we mentioned.

                                                       //Handle the return of a child process from fork1()
                                                       Function fork_exit(callout, arg, frame)
                                                       {
                                                            ...
                                                            callout(arg, frame); // Here callout is fork_return, as we passed it
                                                                                          as a argument to fork_exit

                                                                 Function fork_return() {
                                                                      userret(td, frame);
                                                                 }
                                                       }

                                                  Once we return from fork_exit we will go to doreti
                                                  which will load our frame and we have will go back to
                                                  fork system call as a child. But the chils process will
                                                  not execute right now because we have not scheduled it
                                                  to run. This is the setup we are preparing and once
                                                  all the setup is done we will schedule the child process
                                                  and it will do as described above
                                                  jmp     doreti

                              } // end of cpu_fork()
               } //end of vm_forkproc


     Put child process on run queue
     sched_add(td2, SRQ_BORING);

     ...
     ...

} //end fork1()


Call Tree

center-aligned-image