Mostrando entradas con la etiqueta ps. Mostrar todas las entradas
Mostrando entradas con la etiqueta ps. Mostrar todas las entradas

miércoles, 23 de enero de 2019

When ps let me down

Some tools have been around for ages helping systems administrators during rough times, making our lives and jobs somewhat easier (ok not all the times xD). One of these marvels is, without a doubt, the almighty ps. What if I told you that ps stole a few hours of my life with no remorse, would you believe me? Well... you should xD.

Situation:


Lets assume you had a Linux system and noticed high system load average, along with unusually high CPU utilization. Having a look at the metrics you realize these had been increasing for a few weeks by now.

A quick glance at top, shows something like this:
top - 20:58:27 up  1:18,  3 users,  load average: 50.27, 29.91, 14.09
Tasks: 20215 total,  13 running, 202 sleeping,   0 stopped, 20000 zombie
%Cpu(s): 29.7 us, 70.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
KiB Mem:   3007548 total,  2584796 used,   422752 free,   214556 buffers
KiB Swap:  1048568 total,        0 used,  1048568 free.   983056 cached Mem

 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
25244 juan      20   0   14228   2180   2060 R  14.1  0.1   0:00.77 ps
25246 juan      20   0   14228   2188   2064 R  13.5  0.1   0:00.70 ps
25248 juan      20   0   14228   2036   1916 R  13.5  0.1   0:00.61 ps
25250 juan      20   0   14228   2100   1976 R  13.5  0.1   0:00.57 ps
25252 juan      20   0   14228   2092   1968 R  13.5  0.1   0:00.49 ps
25254 juan      20   0   14228   2180   2056 R  13.2  0.1   0:00.45 ps
25256 juan      20   0   14228   2092   1968 R  11.5  0.1   0:00.39 ps
23469 root      20   0   47308  20812   2572 R  10.9  0.7   1:18.58 top
25258 juan      20   0   14228   2228   2104 R   9.4  0.1   0:00.32 ps
25260 juan      20   0   14228   2212   2088 R   7.4  0.1   0:00.25 ps
25262 juan      20   0   14228   2040   1916 R   5.0  0.1   0:00.17 ps
25264 juan      20   0   14228   2188   2064 R   2.9  0.1   0:00.10 ps
 2290 juan      20   0 2335644 128804  78096 S   1.2  4.3   0:06.22 skypeforli+
    9 root      20   0       0      0      0 S   0.9  0.0   0:05.64 rcuos/0
   18 root      20   0       0      0      0 S   0.9  0.0   0:04.86 rcuos/1
    7 root      20   0       0      0      0 S   0.6  0.0   0:03.59 rcu_sched
 1071 root      20   0  470184 113424  30860 S   0.6  3.8   0:35.99 Xorg

So, what's wrong with this system?


Well, a bunch of things as you can see:

  • To begin with, it kind of looks like an episode of "The Walking Dead", there's 20000 zombie processes xD. Not a big deal, right? zombie processes don't take much memory space and don't cause load increase. However they could easily consume all your process slots within the kernel and that would be a big deal.
  • Then system load is literally insane for 2 vCPUs (13 processes in the running queue). 
  • Adding up to all that, we have several freaking ps processes taking up to 14% of the CPU time each, how come ps can be taking that much CPU time?

Where do the zombies come from?


Unlike human babies, that we all know are carried by the storks, zombie processes are the result of parent processes not cleaning after their terminated childs (more details here). And in this case, we clearly have a good bunch of them, however in this particular scenario I literally faked the zombie processes so we won't go into details as per why they were created.

Why is the load so freaking high?


The reason for the high load is pretty clear, we have tons of processes in the running queue plus potentially some others in D state blocked by some resources. All these would add up to the system load, and what's concerning is the fact that the load was increasing, so at some point this system would have gone unresponsive.

Why in the world is ps taking so much CPU time?


Having a look at the ps processes, looks as if they were trying to pull the state of a given process:
root@test:/home/juan# ps ha -o state,cmd|grep ps
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
R ps ha -o state,cmd
S grep --color=auto ps
R ps h --pid 3234 -o state
R ps h --pid 3234 -o state
root@test:/home/juan#

A pretty harmless move, you would say, so lets time that operation:
root@test:/home/juan# time ps h --pid 3234 -o state
S

real    0m1.003s
user    0m0.188s
sys     0m0.552s
root@test:/home/juan#

wait..., what? took 1 second to run ps? Which age are we living in?

So ps spent a bit more than half of the time running in kernel space, it also spent about ~200 ms in user space and then somewhere around ~300 ms waiting, maybe in the scheduler queue, maybe IO, maybe all of them, who knows.

Half a second spent in the kernel seems to be a lot for such a simple task "get the process state of PID 3234", right? So what's going on?

Strace to the rescue:
root@test:/home/juan# strace -c ps h --pid 3234 -o state
S
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 71.16    0.014693         735        20           getdents
 26.54    0.005481           0     40418           read
  1.54    0.000317           0     40423         4 open
  0.47    0.000097           0     40420           close
  0.30    0.000061           0     20210           stat
  0.00    0.000000           0         1           write
  0.00    0.000000           0         6           fstat
  0.00    0.000000           0         2           lseek
  0.00    0.000000           0        16           mmap
  0.00    0.000000           0         7           mprotect
  0.00    0.000000           0         3           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0        23           rt_sigaction
  0.00    0.000000           0         2           ioctl
  0.00    0.000000           0         4         4 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1           geteuid
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.020649                142959         8 total
root@test:/home/juan#
Turns out it took ps "only 142959 system calls" to get the process state of PID 3234 and that is indeed a pretty insane number of syscalls.

  • ~70% of the time in kernel space was consumed by 20 getdents syscalls, these calls were the ones pulling the directory information from /proc. Keep in mind that /proc is a pretty crowded place when there's ~20k process in the system. So despite these calls not being too many, they actually pulled lots of bytes of data (about 600Kbytes, in 32K sized fragments).
root@test:/home/juan# strace -T -e getdents ps h --pid 3234 -o state
getdents(5, /* 1350 entries */, 32768)  = 32768
getdents(5, /* 1365 entries */, 32768)  = 32760
getdents(5, /* 1365 entries */, 32768)  = 32760
getdents(5, /* 1365 entries */, 32768)  = 32760
getdents(5, /* 1365 entries */, 32768)  = 32760
getdents(5, /* 1024 entries */, 32768)  = 32752
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 1024 entries */, 32768)  = 32768
getdents(5, /* 309 entries */, 32768)   = 9888
getdents(5, /* 0 entries */, 32768)     = 0
+++ exited with 1 +++
root@test:/home/juan#
  • ~26% of the time was spent on read syscalls (40818 calls), these would effectively be the ones reading data from the files within /proc. 
  • The interesting part here is the fact that the number of read, open and close syscalls happen to be almost exactly twice the number of stat calls (20406 calls). And ofcourse the fact that the number of stat calls is alsmot exactly the number of processes in the system.

Wait... are you trying to tell me that ps, despite the fact that you asked for a single process state, is going through all the processes in the system anyways? 

Yeahp, that's exactly what is doing. You don't trust me? (can't blame you, I've been wrong before xD).

That single write syscall in the list above is the one used by ps to show the command output (the process state), right? Ok, so if we dump the strace output to a file we can get a better picture:
root@test:/home/juan# grep ^write -A5 -B5  strace.txt
read(6, "3234 (apache) S 3222 3234 3204 3"..., 1024) = 306
close(6)                                = 0
open("/proc/3234/status", O_RDONLY)     = 6
read(6, "Name:\tapache\nState:\tS (sleeping)"..., 1024) = 780
close(6)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff10e10f000
write(1, "S\n", 2)                      = 2
stat("/proc/3235", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
open("/proc/3235/stat", O_RDONLY)       = 6
read(6, "3235 (apache) Z 3234 3234 3234 3"..., 1024) = 162
close(6)                                = 0
open("/proc/3236/status", O_RDONLY)     = 6
root@test:/home/juan#
Clearly ps is going one by one through all the folders available in /proc and once it comes across the one requested by "--pid" it writes the output, however it keeps moving forward untill all folders are checked (this is disturbingly fun when you try it with PID 1).

Now... why would ps do this?

This seems to be a design decition after all. Ps man states:

"ps - report a snapshot of the current processes."

Note: which suggests ps may actually stand for Processes Snapshot (who knows).

"Except as described below, process selection options are additive. The default selection is discarded, and then the selected processes are added to the set of processes to be displayed. A process will thus be shown if it meets any of the given selection criteria."

So what you actually do with the options is descrcibe to ps which processes you want to print out and which information. Then ps will go through all of them and print out the ones that match your description.

Summary


With all these in mind, looks like using ps to get details of a given proces which PID you already know doesn't seem to be efficient at all, and can be a terrible idea in systems hosting a big number of processes.

After all looks like you would be better of reading the state directly from /proc yourself:
root@test:/home/juan# strace -c grep ^State /proc/3234/status
State:  S (sleeping)
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         5           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0         6           open
  0.00    0.000000           0         9           close
  0.00    0.000000           0         9           fstat
  0.00    0.000000           0         1         1 lseek
  0.00    0.000000           0        14           mmap
  0.00    0.000000           0         8           mprotect
  0.00    0.000000           0         2           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         2         1 ioctl
  0.00    0.000000           0         5         5 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    68         7 total
root@test:/home/juan#

Only took grep 68 syscalls to retrieve the state of the process :P, yeah I know, now I'm just being a jerk hahaha.

domingo, 16 de octubre de 2016

Linux limits 102 - Open files

This post is kind of a follow up of Linux limits 101 - Ulimit from a few weeks ago where we went through ulimit and how they are used to limit users behaviors. This time we'll take a look at another limit the kernel can impose on us and therefore make our lives a bit sower, the number of open files.

I think it's worth mentioning (I may have said this before xD) that when we talk about Open Files in Linux systems we are basically talking about File descriptors (aka file handles). A file descriptor is a data structure used by processes to access: files, Unix sockets, Networking sockets, pipes, etc. Every new process comes by default with 3 file descriptors:
  • FD 0: standard input
  • FD 1: standard output
  • FD 2: standard error
So just by having lets say:
juan@test:~$ ps aux|wc -l
142
juan@test:~$
about 142 processes running on the system, we should expect at least around 426 file descriptors to be in use (142x3=426). What if there was a way to know how many file descriptors a particular process is using?

File descriptors a process is using:


Yeahp, of course that's possible! And as always in Linux, there are at least two different ways. The first approach will be of course the easiest one, each process has a folder under /proc that will provide loads of information, in this case will focus on subfolder fd, where guess what's going to show up? indeed, the file descriptors for the process:
root@test:/home/juan# ls /proc/1/fd | wc -l
24
root@test:/home/juan#
init process (PID=1) has 24 file descriptors in use! We can see more details about them in the next output:
root@test:/home/juan# ls -la /proc/1/fd
total 0
dr-x------ 2 root root  0 Oct 15 10:39 .
dr-xr-xr-x 9 root root  0 Oct 15 10:38 ..
lrwx------ 1 root root 64 Oct 15 10:39 0 -> /dev/null
lrwx------ 1 root root 64 Oct 15 10:39 1 -> /dev/null
lrwx------ 1 root root 64 Oct 15 10:39 10 -> socket:[8662]
lrwx------ 1 root root 64 Oct 15 10:39 11 -> socket:[9485]
l-wx------ 1 root root 64 Oct 15 10:39 12 -> /var/log/upstart/network-manager.log.1 (deleted)
lrwx------ 1 root root 64 Oct 15 10:39 14 -> socket:[10329]
l-wx------ 1 root root 64 Oct 15 10:39 16 -> /var/log/upstart/systemd-logind.log.1 (deleted)
lrwx------ 1 root root 64 Oct 15 10:39 17 -> socket:[8637]
lrwx------ 1 root root 64 Oct 15 10:39 18 -> /dev/ptmx
lrwx------ 1 root root 64 Oct 15 10:39 2 -> /dev/null
lrwx------ 1 root root 64 Oct 15 10:39 20 -> /dev/ptmx
lrwx------ 1 root root 64 Oct 15 10:39 22 -> /dev/ptmx
l-wx------ 1 root root 64 Oct 15 10:39 24 -> /var/log/upstart/modemmanager.log.1 (deleted)
lrwx------ 1 root root 64 Oct 15 10:39 29 -> /dev/ptmx
lr-x------ 1 root root 64 Oct 15 10:39 3 -> pipe:[8403]
lrwx------ 1 root root 64 Oct 15 10:39 30 -> /dev/ptmx
l-wx------ 1 root root 64 Oct 15 10:39 31 -> /var/log/upstart/mysql.log.1 (deleted)
lrwx------ 1 root root 64 Oct 15 10:39 34 -> /dev/ptmx
lrwx------ 1 root root 64 Oct 15 10:39 36 -> /dev/ptmx
l-wx------ 1 root root 64 Oct 15 10:39 4 -> pipe:[8403]
lr-x------ 1 root root 64 Oct 15 10:39 5 -> anon_inode:inotify
lr-x------ 1 root root 64 Oct 15 10:39 6 -> anon_inode:inotify
lrwx------ 1 root root 64 Oct 15 10:39 7 -> socket:[8404]
lrwx------ 1 root root 64 Oct 15 10:39 9 -> socket:[12675]
root@test:/home/juan#
we can see how the file descriptors are represented as links, a brief of the output would be:
  • Default file descriptors (0,1 and 2) have been pointed to /dev/null, which is ok for a process like init that isn't an interactive process.
  • There are a couple of UNIX sockets "socket:[XXXX]" opened (7,9,10, 11, etc), probably to connect to other processes.
  • There's a pipe "pipe:[8403]" as well using two fd (3 and 4) that's normal, pipes provide a fd to write and one to read while data is buffered on the kernel.
  • The rest of the fs point to:
    • /dev/ptmx pseudo terminal device.
    • inotify a way to monitor changes on files, this means init is interested in the events on two particular fd.
    • some deleted log files like /var/log/upstart/mysql.log.1 this is odd. Probably files were rotated or something like that.
 If for some reason these details weren't enough, you can go hardcore and try the second way.  lsof (list open files, makes sense, right?) is the program you need for that.

Lets list all the open files for a particular process using lsof, init in this case:
root@test:/home/juan# lsof -p 1
lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/112/gvfs
      Output information may be incomplete.
COMMAND PID USER   FD   TYPE             DEVICE SIZE/OFF   NODE NAME
init      1 root  cwd    DIR                8,1     4096      2 /
init      1 root  rtd    DIR                8,1     4096      2 /
init      1 root  txt    REG                8,1   265848 261189 /sbin/init
init      1 root  mem    REG                8,1    43616 581960 /lib/x86_64-linux-gnu/libnss_files-2.19.so
init      1 root  mem    REG                8,1    47760 555508 /lib/x86_64-linux-gnu/libnss_nis-2.19.so
init      1 root  mem    REG                8,1    97296 555504 /lib/x86_64-linux-gnu/libnsl-2.19.so
init      1 root  mem    REG                8,1    39824 555503 /lib/x86_64-linux-gnu/libnss_compat-2.19.so
init      1 root  mem    REG                8,1    14664 555500 /lib/x86_64-linux-gnu/libdl-2.19.so
init      1 root  mem    REG                8,1   252032 540246 /lib/x86_64-linux-gnu/libpcre.so.3.13.1
init      1 root  mem    REG                8,1   141574 555505 /lib/x86_64-linux-gnu/libpthread-2.19.so
init      1 root  mem    REG                8,1  1840928 581957 /lib/x86_64-linux-gnu/libc-2.19.so
init      1 root  mem    REG                8,1    31792 581956 /lib/x86_64-linux-gnu/librt-2.19.so
init      1 root  mem    REG                8,1    43464 527349 /lib/x86_64-linux-gnu/libjson-c.so.2.0.0
init      1 root  mem    REG                8,1   134296 527439 /lib/x86_64-linux-gnu/libselinux.so.1
init      1 root  mem    REG                8,1   281552 527323 /lib/x86_64-linux-gnu/libdbus-1.so.3.7.6
init      1 root  mem    REG                8,1    38920 527371 /lib/x86_64-linux-gnu/libnih-dbus.so.1.0.0
init      1 root  mem    REG                8,1    96280 527373 /lib/x86_64-linux-gnu/libnih.so.1.0.0
init      1 root  mem    REG                8,1   149120 555506 /lib/x86_64-linux-gnu/ld-2.19.so
init      1 root    0u   CHR                1,3      0t0   1029 /dev/null
init      1 root    1u   CHR                1,3      0t0   1029 /dev/null
init      1 root    2u   CHR                1,3      0t0   1029 /dev/null
init      1 root    3r  FIFO                0,9      0t0   8403 pipe
init      1 root    4w  FIFO                0,9      0t0   8403 pipe
init      1 root    5r  0000               0,10        0   7661 anon_inode
init      1 root    6r  0000               0,10        0   7661 anon_inode
init      1 root    7u  unix 0xffff8800b37c8780      0t0   8404 @/com/ubuntu/upstart
init      1 root    9u  unix 0xffff8800a14e7c00      0t0  12675 @/com/ubuntu/upstart
init      1 root   10u  unix 0xffff8800b37c9a40      0t0   8662 @/com/ubuntu/upstart
init      1 root   11u  unix 0xffff8800b37a1e00      0t0   9485 @/com/ubuntu/upstart
init      1 root   12w   REG                8,1      283 551619 /var/log/upstart/network-manager.log.1 (deleted)
init      1 root   14u  unix 0xffff8800b37a3c00      0t0  10329 @/com/ubuntu/upstart
init      1 root   16w   REG                8,1      451 522345 /var/log/upstart/systemd-logind.log.1 (deleted)
init      1 root   17u  unix 0xffff8800b37cb0c0      0t0   8637 socket
init      1 root   18u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   20u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   22u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   24w   REG                8,1      502 527289 /var/log/upstart/modemmanager.log.1 (deleted)
init      1 root   29u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   30u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   31w   REG                8,1      881 552236 /var/log/upstart/mysql.log.1 (deleted)
init      1 root   34u   CHR                5,2      0t0   1932 /dev/ptmx
init      1 root   36u   CHR                5,2      0t0   1932 /dev/ptmx
root@test:/home/juan#
now we can see a few more things, like:
  • Details of the FD, like its Type, Device it belongs to, etc.
  • We can see also some things that aren't really opened fd but some extra process information:
    • cwd current working directory
    • rtd root directory
    • txt init's binary code file
    • memory mapped files in this case bunch of system libraries. YES, these had a fd when they were mapped, but the fd was closed right after the mmap call was successful (you can see that checking this entry about strace).
Ok, now we know what a file descriptor or file handle is and how to identify them and map them to our processes. Is there any system wide limit for the file descriptors you can open?
 

Max open files, system wide:


If the answer for the previous question was now, I wouldn't have a reason to write this article in the first place I guess xD, therefore the answer is YES :P. The kernel is cool, and in order to play safe it has to set limits (I sound like a father now...) to avoid bigger problems.

The maximum number of open files the kernel can handle can be obtained from our beloved /proc, particularly in file file-nr under sys/fs directory. Here we can see the numbers for my current system:
root@test:/home/juan# cat /proc/sys/fs/file-nr
2944 0 298505
root@test:/home/juan#
These values mean the following:
  • First value (2944) indicates the number of allocated file descriptors, these are allocated dynamically by the kernel.
  • Second value (0) is the number of allocated but unused file descriptor. Kernels from 2.6.something free any unused fd, so this value should always be 0.
  • Third value (298505) indicates the maximum number of file descriptors that the kernel can allocate (also visible on file-max file).
Summing up, there are 2944 file descriptors allocated and in use at this precise moment. Almost 3k file descriptors allocated for about 142, interesting right?

Just for the sake of it, lets track down the process using the most number of file descriptors:
root@test:/home/juan# for i in `ps -Ao pid|grep -v PID`;do count=`ls /proc/$i/fd/ 2> /dev/null|wc -l`; echo "$count $i";done | sort -nr | head
61 1075
48 393
32 1264
31 1365
27 1132
24 1
21 440
20 1265
19 1325
19 1311
root@test:/home/juan#
there we see the top ten (first column is the number of FD and the second is the PID). Interestingly enough there's a process using 61 file descriptors, turns out I had mysqld installed on this VM (had no idea...):
root@test:/home/juan# ps aux|grep 1075
mysql     1075  0.1  1.9 624040 57792 ?        Ssl  10:38   0:07 /usr/sbin/mysqld
root      9664  0.0  0.0  15948  2232 pts/1    S+   12:35   0:00 grep --color=auto 1075
root@test:/home/juan#
 

Increasing the limit


If by any chance the almost 300k file descriptors the kernel allows to open is not enough (some busy systems may reach that limit) you will notice logs like "VFS: file-max limit reached" on dmesg and probably in messages or syslog files. In that case,  you can increase that limit using one of the following ways:
  • iminproductionpainchangeitrightnowgoddamnit way, by just updating /proc with the new value, like
root@test:/home/juan# echo 400000 > /proc/sys/fs/file-max
root@test:/home/juan# cat /proc/sys/fs/file-nr
3072    0       400000
root@test:/home/juan#
  • Or you can be more elegant and use sysctl command:
root@test:/home/juan# sysctl -w fs.file-max=500000
fs.file-max = 500000
root@test:/home/juan# cat /proc/sys/fs/file-nr
3072    0       500000
root@test:/home/juan#

In any case, don't forget to make the change persistent by doing:
root@test:/home/juan# echo "fs.file-max=500000" >> /etc/sysctl.conf
root@test:/home/juan#

If the error showing on your logs is instead "Too many open files" then the limit you've reached is most likely the ulimit for the user :D, which you know how to deal with because you've read this.

And that's about it! 

viernes, 20 de noviembre de 2015

Zombie Processes - Procesos Zombies

No, no se trata de procesos que se mueven de manera errática y que convierten a otros procesos si los muerden (aunque sería muy divertido).

Cuando hablamos de procesos zombies nos referimos a procesos que han completado su ejecución (estado terminated) pero aun se encuentran registrados en la tabla de procesos del kernel. ¿Por qué sucede esto? Básicamente sucede para poder garantizar que el proceso padre pueda obtener el estado final de sus procesos hijos, para saber cuál fue el resultado. Una vez que el proceso padre lee el estado de salida del hijo (a través de la syscall wait) este último será removido de la tabla de procesos y podrá descansar finalmente en paz, e ir al cielo de los procesos.

Algunos puntos para resaltar:
  • Un proceso zombie es un proceso cuya ejecución ha finalizado y el estado del proceso es TERMINATED. Puede haber terminado por las buenas o por las malas (con kill por ejemplo).
  • La memoria ocupada por el proceso ha sido liberada.
  • Podemos ver procesos zombies utilizando ps aux, los reconoceremos por la Z en la columna de STAT.
  • Todo proceso que termina su ejecución se vuelve zombie, aunque raras veces lo notaremos dado que por lo general el proceso padre estará esperándolo con la llamada wait.

Creando zombies


A modo de prueba de concepto veamos el siguiente código, que será nuestro generador de zombies:

#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int pid_status;
        pid = fork();
        if (pid == 0) {
                sleep(10);
                exit(9);
        }
        printf("PID del hijo %d\n",pid);
        sleep(40);
        waitpid(pid, &pid_status, 0);
        printf("Estado de salida %d\n",WEXITSTATUS(pid_status));
        return 0;
}

El código es sencillo, luego de la llamada a fork tendremos en el sistema corriendo dos procesos idénticos. Uno de ellos, el hijo, ejecutará el código dentro del if, mientras que el padre irá directamente al primer printf, luego  dormirá por 40 segundos y ejecutará waitpid para obtener el estado de terminación del hijo e imprimirlo.

Veamos un poco qué sucede durante la ejecución:

[ec2-user@ip-172-31-16-177 ~]$ gcc -o zombies zombies.c
[ec2-user@ip-172-31-16-177 ~]$ ./zombies
PID del hijo 23037
Estado de salida 9
[ec2-user@ip-172-31-16-177 ~]$

El proceso padre imprimió el PID del hijo (23037), luego durmió por 40 segundos, ejecutó waitpid para recojer el valor de salida del proceso hijo (9) y lo imprimió en pantalla. Usando ps antes de que termine la ejecución del proceso hijo podemos ver un poco mas en detalle lo que sucede:

La relación entre los procesos (23036 es el proceso padre y 23037 es el hijo):

[ec2-user@ip-172-31-16-177 ~]$  ps -eo pid,ppid,cmd|grep zombie
23036  2407 ./zombies
23037  23036 ./zombies
23293 22781 grep --color=auto zombie
[ec2-user@ip-172-31-16-177 ~]$

Luego aun dentro de los primeros 10 segundos vemos lo siguiente:

[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23036  0.0  0.0   4176   620 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23037  0.0  0.0   4172    80 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23039  0.0  0.2 110460  2196 pts/1    S+   22:22   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$

ambos procesos se encuentran en estado Sleeping (S+), y podemos ver también que ambos tienen el mismo valor de VSZ (virtual memory size), lo cual tiene sentido dado que son idénticos luego de la llamada fork. Pasados los 10 segundos nos encontramos con la siguiente situación:

[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23036  0.0  0.0   4176   620 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23037  0.0  0.0      0     0 pts/0    Z+   22:22   0:00 [zombies] <defunct>
ec2-user 23041  0.0  0.2 110460  2124 pts/1    S+   22:22   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$

el proceso hijo se encuentra ahora en Zombie state (Z+) y su VSZ es 0 (dado que toda la memoria ocupada fue liberada).

Una vez que se cumplen los 40 segundos, el proceso padre y el hijo desaparecen del sistema.


[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
[ec2-user@ip-172-31-16-177 ~]$

Cómo matar un proceso zombie?


Como se imaginaran, cortarle la cabeza al proceso no parece ser una opción viable en este contexto. El equivalente en el mundo de los procesos es la señal SIGKILL (9), pero veamos qué sucede cuando la usamos para matar un proceso zombie:

Ejecución del binario zombie:

[ec2-user@ip-172-31-22-1 ~]$ ./zombie
PID del hijo 2765
Estado de salida 9
[ec2-user@ip-172-31-22-1 ~]$


Intentos fallidos de acabar con la existencia del proceso 2765:

[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0   4172    80 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2769  0.0  0.0 110460  2196 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2771  0.0  0.0 110460  2152 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$
[ec2-user@ip-172-31-22-1 ~]$ kill -9 2765
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2773  0.0  0.0 110460  2140 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$ kill -9 2765
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2775  0.0  0.0 110460  2192 pts/0    S+   21:07   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$


Claramente kill -9 no esta siendo capaz de terminar el proceso, la señal está siendo enviada al proceso  y no recibimos ningun mensaje de error o algo similar.

Cómo podríamos deshacernos de ellos?


Lamentablemente, la única manera de deshacernos de ellos es matando el proceso padre. Dando muerte al proceso padre, los procesos zombies se convierten en procesos huérfanos y serán adoptados por init, luego init ejecutará waitpid y los procesos descansarán finalmente en paz.

A continuación un pequeño ejemplo:

[ec2-user@ip-172-31-16-177 ~]$  ps -eo pid,ppid,cmd|grep zombie
23346  2407 ./zombies1
23347 23346 ./zombies1
23348 23346 ./zombies1
23349 23346 ./zombies1
23350 23346 ./zombies1
23351 23346 ./zombies1
23353 22781 grep --color=auto zombie
[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23346  0.0  0.0   4172   600 pts/0    S+   23:40   0:00 ./zombies1
ec2-user 23347  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]
ec2-user 23348  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23349  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23350  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23351  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23355  0.0  0.2 110460  2124 pts/1    S+   23:41   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$ kill -9 23346
[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23357  0.0  0.2 110460  2156 pts/1    S+   23:41   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$


Podemos ver que tenemos 5 procesos en estado zombie y cuyo padre es el proceso 23346. Una vez que enviamos la señal (con kill) para matar el proceso padre, todos los hijos desaparecen.

El código utilizado fue el siguiente:

#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int pid_status=0;
        int i;
        for(i=0;i<5;i++)
        {
                pid = fork();
                if (pid == 0) {
                        sleep(3);
                        exit(9);
                }
        }
        sleep(600);
        return 0;
}

Por suerte, dado que los procesos Zombies son proceso que técnicamente ya no se encuentran consumiendo recursos (salvo por las estructuras de kernel donde se encuentran representados) no deberíamos preocuparnos demasiado. Sin embargo podríamos caer en una situación donde tenemos muchos de ellos acumulados...

Zombie fork bomb!


Ok, los procesos en estado zombie en teoría no consumen memoria, pero siguen estando representados dentro del kernel de alguna manera (de lo contrario no serian visibles para ps por ejemplo) por lo que algo de memoria deben consumir. Qué sucede si creamos tantos zombies como procesos podemos crear según ulimit?

[ec2-user@ip-172-31-22-1 ~]$ ulimit -u
31877
[ec2-user@ip-172-31-22-1 ~]$


Según ulimit este usuario puede crear hasta 31877 procesos,  de los cuales ya hay:

[ec2-user@ip-172-31-22-1 ~]$ ps aux | grep ^ec2-user | wc -l
6[ec2-user@ip-172-31-22-1 ~]$


Entonces con el proceso padre hacemos 7 y por lo tanto veamos qué pasa si ocupamos todos los procesos, intentemos crear 31870 zombies (seguro me van a llamar de The walking dead después de esto), usando el siguiente código:

#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int bomb;
        for(bomb=0;bomb<31870;bomb++)
        {
                pid = fork();
                if (pid == 0) {
                        sleep(30);
                        exit(9);
                }
        }
        getchar();
        return 0;
}

Ejecutamos (abrí una consola como root, just in case):

[ec2-user@ip-172-31-22-1 ~]$ gcc -o zombie_fork_bomb zombie_fork_bomb.c
[ec2-user@ip-172-31-22-1 ~]$ ./zombie_fork_bomb

Y desde la consola de root vemos lo siguiente:

[root@ip-172-31-22-1 ec2-user]# free -m
             total       used       free     shared    buffers     cached
Mem:          7987        417       7569          0         13        320
-/+ buffers/cache:         83       7903
Swap:            0          0          0

[root@ip-172-31-22-1 ec2-user]# ps aux|grep ^ec2-user|wc -l
31877
[root@ip-172-31-22-1 ec2-user]# ps aux|
grep ^ec2-user|grep " Z+ "|wc -l
31870
[root@ip-172-31-22-1 ec2-user]# free -m
             total       used       free     shared    buffers     cached
Mem:          7987       1445       6541          0         13        320
-/+ buffers/cache:       1111       6875
Swap:            0          0          0
[root@ip-172-31-22-1 ec2-user]#


Se pudieron los 31870 zombies y con eso alcanzamos el limite de los 31877 procesos disponibles para el usuario ec2-user. Eso lo podemos comprobar con la segunda consola del usuario:

[ec2-user@ip-172-31-22-1 ~]$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
^C-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable


[ec2-user@ip-172-31-22-1 ~]$


Claramente bash no esta pudiendo hacer el fork para ejecutar ls. Por otro lado también podemos ver que el consumo de memoria aumento considerablemente de 417MBytes a 1445MBytes, prácticamente triplicado, pero claro, estamos hablando de 31871 procesos en estado zombie.

Con esto podemos inferir que cada proceso en estado zombie nos cuesta alrededor de 33Kbytes de memoria ((1445-417)/31871*1024 Kbytes) y lo que es tal vez mas importante, cuenta como un proceso más en ejecución a la hora de controlar los límites del usuario.

Conclusión:

Los procesos zombies solo pueden ser eliminados si su padre o init ejecutan la función wait para ese process ID. A pesar de que los procesos zombies son proceso cuya ejecución ha finalizado, siguen ocupando lugar en el kernel y podrían ocasionar problemas mayores si se tratara de un gran numero de ellos.

Como breve comentario final, no hay que confundir los proceso en estado Z (zombie) con los procesos en estado D (uninterruptible). Estos últimos por lo general se encuentran esperando alguna operación de E/S y por lo tanto no hay manera de interrumpirlos. Deshacerse de uno de estos procesos es BASTANTE mas complicado y podría llegar a ser necesario reiniciar el sistema. En otra entrada vamos a ver de que se trata eso.