Pelicanux

Just A Few Random Words

Forks and Python

I had once to run a script which continuously checks for some stuff during the whole system lifetime. Sometimes, inotify is not enough, so I wrote a basic python script to create a daemon doing the job.

Basic usecase

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python

import os,sys,time

def daemon():
  time.sleep(5)
  with open('/tmp/essai', 'w') as f:
      f.write('written by daemon')

if os.fork() == 0:
  daemon()

sys.exit(0)

At first, I didn’t pay too much attention to the double fork stuff. All I cared was running this function on background. And until I understand it, I was always wondering whether or not this script was efficient.

To provide some explication, I have first to explain what a zombie process is. Every process terminates with a return-code. And only the process responsible for its creation, its father-process, has access to this return-code when its child dies. A process becomes a zombie when it terminates, but init can’t get its return-code. Init can’t know for sure whether or not the process is dead, because, even if it looks like dead, well, it does not return anything.

Now, the father creates its child, and this creation is not blocking (well, I could say: per definition; this is the very purpose of forking). Which means, the father won’t wait for its child to die, but will perform its own tasks and so does its child. And when the child has finished with them, its returns its return-code to its father. But, remember? Only the father knows it, and init won’t. So, at this time, and until the father itself dies, the child turns into a zombie for init.

So, was my script valid? Well, once created, the child waits (and this wait is blocking) for 5 seconds. So, the child has at least 5 seconds of lifetime. And the father exits (os.fork() == 0 for the father), leaving its child orphean. As consequence, init rechild the child process and will then get its status-code once its destiny is complete. I don’t create any zombie here.

To conclude with, single-forking is enough when the father dies in short notice after child creation.

Writing a Zombie on purpose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python

import os,sys,time

LOG='/tmp/essai/log'

def daemon():
   i = 1
   while i<15:
      time.sleep(1)
      with open(LOG, 'a') as f:
         f.write('iteration '+str(i)+'\n')
      i+=1

if os.fork() > 0:
   time.sleep(100)
   sys.exit(0)

daemon()
  • A first, everything goes well and daemon writes stuff into logfile.
  • When i=15 daemon dies. But father remains and init can’t get child’s exist status.
  • Output of ps aufx | grep python command:
1
2
3
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
me  7574  0.0  0.1  23664  4516 pts/2    S+   21:29   0:00  |   \_ python fork.py
me  7575  0.0  0.0      0     0 pts/2    Z+   21:29   0:00  |       \_ [python] <defunct>

RSS and VSZ turns to 0 (no memory is getting used)

When the father still has stuff to do

Indeed, things are getting more complicated when the father as other tasks to perform. Because the father won’t let init rechild its child until its dies. In this case, the solution is to fork twice: – First the father forks – Then the child forks and dies . Once again, forking is not blocking, it dies immediately afte forking – The grand-child is orphean as soon as it is born; So immediately rechild by init – Father and grand-child may now die safely.

Here is the code showing how to do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python

import os,sys,time

def daemon():
  time.sleep(5)
  with open('/tmp/essai', 'w') as f:
      f.write('written by daemon')

if os.fork() == 0:
  # Here is the first child
  if os.fork() == 0:
      # Here is the second child
      daemon()

# Father's blocking stuff
time.sleep(100)

Is there anything left?

  • Well, first of all, it’s safer to check for the success or failure of the os.fork system call. Because it may simply throw exceptions (the maximum number of PID may be reached for instance).
  • Then, we must ensure the child does not inherit its father environment, such as working directory (The working directory may be a mounted filesystem, and the running daemon inside may prevent the system to umount the filesystem at shutdown time), umask (A child process created via fork(2) inherits its parent’s umask man says) and create a new session with os.setsid().
  • _exit instead of exit

Final version

This version is for most parts inspired from this blog

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
try:
  p = os.fork()
except OSError,e:
  raise Exception, "%s [%d]" % (e.strerror, e.errno)

if p == 0:

  # Decouple from parent's environment
  os.setsid()
  os.chdir('/')
  os.umask(0)

  try:
      q = os.fork()
  except OSError,e:
      raise Exception, "%s [%d]" % (e.strerror, e.errno)

  if q == 0:
      
      # Redirect standard file descriptors
      # Since daemon has no controling terminal,
      # it has to redirect stdin, stdout, stderr to /dev/null
      # This prevents side effects of reading/writing to the standard
      # I/O file descriptors
      sys.stdout.flush()
      sys.stderr.flush()
      si = file('/dev/null', 'r')
      so = file('/dev/null', 'a+')
      se = file('/dev/null', 'a+', 0)
      os.dup2(si.fileno(), sys.stdin.fileno())
      os.dup2(so.fileno(), sys.stdout.fileno())
      os.dup2(se.fileno(), sys.stderr.fileno())

      # Close open file descriptors.
      # This prevent child inherits from parent open file descriptors
      try:
          maxfd = os.sysconf("SC_OPEN_MAX")
      except (AttributeError, ValueError):
          maxfd = 1024
      for fd in range(0, maxfd):
          try:
              os.close(fd)
          except OSError:   # fd not open, nothing to do
              pass

      daemon()
  else:
      os._exit(0)

else:
  # Here is the parent.
  # Doing his stuff or simply exiting
  os._exit(0)

Sources

Here are a few sources which provided great help understanding the principe and creating this article: