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

lunes, 30 de abril de 2018

Where do your bytes go when you send them to /dev/null?


I've been playing around with some interesting tools lately so decided to use one of them to answer a rather ontological question. No, I'm not talking about the meaning of life and stuff, but the one on the title!

I'm pretty sure you have at least once come across the idea of running some IO benchmark (I didn't say this is a good idea, but lets face it... you've done it) of your drive and decided to run something like the following:

dd if=/home/juan/myfile of=/dev/null

You know, trying to measure the read throughput of the device where myfile file is stored. Or maybe you've redirected some annoying stderr output to prevent it from showing up on your stdout. The point is that I've always blindly assumed /dev/null is a kind of black hole or one way ticket for your bytes, but never actually understood how it works.

Spoiler alert, I've written this article while working on my Ubuntu Trusty VM, so all the kernel source links will be pointing to kernel 3.19 however, most (if not all) of the code/content should be valid in newer kernels. If it's not valid then, sorry xD I'm not going write this again, so get your hands dirty and read some source code.

What's /dev/null after all?


According to Wikipedia:

The null device is typically used for disposing of unwanted output streams of a process, or as a convenient empty file for input streams. This is usually done by redirection.

Something interesting is the concept of "device", /dev/null isn't a regular file, but a special device file, particularly a character device file. You may have heard the concept of "in Unix everything is a file" (ok, except Network cards :P), that's quite accurate and there are several different file types, being the most common the following:

  • Regular files
  • Directory
  • Named Pipe
  • Socket
  • Symbolic Link
  • Device file
    • Character
    • Block
Lets have a quick look at this special file and see what's so special about it! Using stat we already have some nice details about it:

juan@test:~$ stat /dev/null
  File: ‘/dev/null’
  Size: 0             Blocks: 0          IO Block: 4096   character special file
Device: 6h/6d    Inode: 1029        Links: 1     Device type: 1,3
Access: (0666/crw-rw-rw-)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2018-04-17 18:57:48.359574000 +0100
Modify: 2018-04-17 18:57:48.359574000 +0100
Change: 2018-04-17 18:57:48.359574000 +0100
 Birth: -
juan@test:~$

What does "Device type: 1,3"  stand for? Well that's the key here, these two numbers are the Major (identifies the driver used) and the Minor number (identifies the device).

According to Linux Devices documentation, the character device with Major number 1 and Minor Number 3 is the Memory character device /dev/null (aka Null device). If you feel like getting your hands really dirty, you can see the Memory character implementation code here :P.

So what happens when you send stuff to /dev/null?


As with every file intervention, at least the following things have to happen:

- Open the file and get a file descriptor
- Write to the file using the file descriptor obtained previously
- Close the file using the file descriptor obtained previously

these 3 actions require kernel intervention and they map directly to syscalls open, write and close. Lets have a look at this with strace:

root@test:/home/juan# strace -e "open,read,write,close,dup2" dd if=/dev/zero of=/dev/null count=1
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P \2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
open("/dev/zero", O_RDONLY)             = 3
dup2(3, 0)                              = 0
close(3)                                = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
dup2(3, 1)                              = 1
close(3)                                = 0
read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 512) = 512
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 512) = 512
close(0)                                = 0
close(1)                                = 0
open("/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 0
read(0, "# Locale name alias data base.\n#"..., 4096) = 2570
read(0, "", 4096)                       = 0
close(0)                                = 0
open("/usr/share/locale/en_IE/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en_IE/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en/LC_MESSAGES/coreutils.mo", O_RDONLY) = 0
close(0)                                = 0
write(2, "1+0 records in\n1+0 records out\n", 311+0 records in
1+0 records out
) = 31
write(2, "512 bytes (512 B) copied", 24512 bytes (512 B) copied) = 24
write(2, ", 0.000607882 s, 842 kB/s\n", 26, 0.000607882 s, 842 kB/s
) = 26
close(2)                                = 0
+++ exited with 0 +++
root@test:/home/juan# 

So /dev/null file was opened and in return we got FD 3, which was then copied to FD 1 (dup2(3,1)). Right after this, write call was used to write on it, we can see the exact lines here:

open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
dup2(3, 1)                              = 1
close(3)                                = 0
read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 512) = 512
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 512) = 512

Once the process hands over control to the kernel to execute the syscalls code we totally loose track of what's going on. To be honest that's the whole point of an API like the syscall layer, however sometimes is good to have the chance to see how things work in the background, and in order to answer the question we need to understand it.

Ftrace, like strace but way more fun!


Ftrace was introduced on the Linux kernel long ago, somewhere around late 2008 on Kernel 2.6.27. It allows users to trace certain Kernel functions given an incredible visibility of what's going and how things really work.

In order for ftrace to be available, the kernel needs to be compiled with this tracing feature. You can confirm by looking for CONFIG_FUNCTION_TRACER, like:

juan@test:~$ grep FUNCTION_TRACER /boot/config-3.19.0-51-generic
CONFIG_HAVE_FUNCTION_TRACER=y
CONFIG_FUNCTION_TRACER=y
juan@test:~$

Many distributions ship their kernels with ftrace enabled and even the debugfs (the way to access ftrace data) file system mounted (Ubuntu for example):

juan@test:~$ mount | grep debug
none on /sys/kernel/debug type debugfs (rw)
juan@test:~$

What essentially happens is pretty much summed up in these lines:

It uses a compiler feature to insert a small, 5-byte No-Operation instruction to the beginning of every kernel function, which NOP sequence is then dynamically patched into a tracer call when tracing is enabled by the administrator. 

so these few extra bytes are used later on to hook a tracing function that will gather the details of the invoked functions, pretty neat!

There's a LOT of ftrace documentation around so I won't waste time repeating any of it. I do suggest you to read it though. In this particular case I'll be using trace-cmd frontend tool instead of directly use the files under the debugfs directory.

BTW: ftrace is a great way to inspect and learn about the Linux Kernel, so do give it a go.

SHOW ME THE MONEY!

Ok, ok,... In order to track the kernel functions I'm going to use function_graph plugin with a little of filtering to only get the events related to the write syscall:

root@test:/home/juan# trace-cmd record -p function_graph -g "SyS_write" dd if=/dev/zero of=/dev/null count=1
  plugin 'function_graph'
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.00255381 s, 200 kB/s
Kernel buffer statistics:
  Note: "entries" are the entries left in the kernel ring buffer and are not
        recorded in the trace data. They should all be zero.

CPU: 0
entries: 0
overrun: 0
commit overrun: 0
bytes: 2476
oldest event ts: 180098.264687
now ts: 180098.267521
dropped events: 0
read events: 468

CPU: 1
entries: 0
overrun: 0
commit overrun: 0
bytes: 2184
oldest event ts: 180098.266223
now ts: 180098.267677
dropped events: 0
read events: 1882

CPU0 data recorded at offset=0x42b000
    20480 bytes in size
CPU1 data recorded at offset=0x430000
    77824 bytes in size
root@test:/home/juan# 

The execution of the command creates a "trace.dat" file were the captured events are written. Then you can access to these events using "trace-cmd report". Since we know we are looking for the first Write call done by dd command I will limit the output of the report to the following:

root@test:/home/juan# trace-cmd report | grep "dd-" | head -25
              dd-3770  [001] 180098.263240: funcgraph_entry:                   |  SyS_write() {
              dd-3770  [001] 180098.263269: funcgraph_entry:                   |    __fdget_pos() {
              dd-3770  [001] 180098.263269: funcgraph_entry:        0.077 us   |      __fget_light();
              dd-3770  [001] 180098.263270: funcgraph_exit:         0.669 us   |    }
              dd-3770  [001] 180098.263270: funcgraph_entry:                   |    vfs_write() {
              dd-3770  [001] 180098.263270: funcgraph_entry:                   |      rw_verify_area() {
              dd-3770  [001] 180098.263271: funcgraph_entry:                   |        security_file_permission() {
              dd-3770  [001] 180098.263271: funcgraph_entry:                   |          apparmor_file_permission() {
              dd-3770  [001] 180098.263271: funcgraph_entry:                   |            common_file_perm() {
              dd-3770  [001] 180098.263272: funcgraph_entry:        0.087 us   |              aa_file_perm();
              dd-3770  [001] 180098.263272: funcgraph_exit:         0.679 us   |            }
              dd-3770  [001] 180098.263272: funcgraph_exit:         1.230 us   |          }
              dd-3770  [001] 180098.263273: funcgraph_exit:         1.783 us   |        }
              dd-3770  [001] 180098.263273: funcgraph_exit:         2.330 us   |      }
              dd-3770  [001] 180098.263273: funcgraph_entry:        0.068 us   |      write_null();
              dd-3770  [001] 180098.263274: funcgraph_entry:                   |      __fsnotify_parent() {
              dd-3770  [001] 180098.263274: funcgraph_entry:        0.103 us   |        dget_parent();
              dd-3770  [001] 180098.263275: funcgraph_entry:        0.078 us   |        dput();
              dd-3770  [001] 180098.263275: funcgraph_exit:         1.255 us   |      }
              dd-3770  [001] 180098.263276: funcgraph_entry:                   |      fsnotify() {
              dd-3770  [001] 180098.263276: funcgraph_entry:        0.308 us   |        __srcu_read_lock();
              dd-3770  [001] 180098.263277: funcgraph_entry:        0.087 us   |        __srcu_read_unlock();
              dd-3770  [001] 180098.263277: funcgraph_exit:         1.548 us   |      }
              dd-3770  [001] 180098.263278: funcgraph_exit:         7.318 us   |    }
              dd-3770  [001] 180098.263278: funcgraph_exit:         9.498 us   |  }
root@test:/home/juan#

Pretty neat, right? we can clearly see the entry and exit of "every" kernel function within the SyS_write syscall. Now, with the function names we can go to our one and only https://elixir.bootlin.com and read the code of each function to have a better picture:
  • SyS_write Write syscall enter
  • vfs_write VFS generic write path, security checks and permissions validation (among other things :))
  •  write_null this function is the write function for the specific device /dev/null (indicated by the file_operations structure of the device), if this was a regular file we would see here a File System specific write function.
Now if we explore the code of write_null, we can see the following:

static ssize_t write_null(struct file *file, const char __user *buf,
     size_t count, loff_t *ppos)
{
 return count;
}

the parameters:
  • *file the file structure (created during the open syscall and linked to the fd)
  • *buf the buffer that keeps the data that is supposed to be written
  • count the number of bytes to write 
  • *ppos the position within the file where to start writing. 

As you can see the function itself doesn't do a thing (which was kind of expected after all xD), it just returns the number of bytes that were supposed to be written as some sort of acknowledgement and confirming the write operation did actually take place.

So not only now you know about /dev/null :P but you can go and have fun with ftrace exploring the Linux Kernel!!!

To answer the question, your bytes literally go nowhere!

lunes, 15 de mayo de 2017

We all understand free command... don't we???

I know I should be writing the next entry of "Understanding SOMAXCONN parameter Part I" (and I am) but instead I decided to do a little detour first. I faced this question about free a few days ago and noticed odd/different answers for it (including my own xD) so decided to go for a deep dive and found some interesting facts worth sharing.

Free


Free Linux command is one of those tools you will be definitely get exposed to as a sysadmin, and understanding its output is extremely important. A bad interpretation of free output could cause unnecessary panic and you don't want to panic for no reason :D (oh wait... maybe you do, I certainly don't ).

Lets have a look at a simple free output:

juan@test:~$ free
             total       used       free     shared    buffers     cached
Mem:       3007572     584872    2422700      14780      55808     291704
-/+ buffers/cache:     237360    2770212
Swap:            0          0          0
juan@test:~$

In the line starting with "Mem:" we get to see the total available physical memory on the system 3007572 KBytes (3GBytes) and then how it is distributed:
  • used: memory being occupied
  • free: memory that is available in the system
  • shared: amount of memory that is shared between processes
  • buffers: memory used for kernel buffers involving IO (disk, network, etc)
  • cached: memory used for data caching
Then we have a line starting with "-/+ buffers/cache:" which looks kind of cryptic, doesn't it?... it turns out to be a kind of summary of the previous line:
  • There are 237360 KBytes being used, this number is the result of the following math "used - (buffers + cached)". This is the amount of memory the system is actually relying on.
  • There are 2770212 KBytes that could be utilized, this is the result of "free + buffers + cached". What this number actually means is that the kernel should be able to reclaim buffer and cached memory if necessary.
So we have a better understanding of free output now, but lets get more details of where these numbers come from.

The power of the force source


Free binary is shipped as part of procps-ng package in Linux and we can inspect its latest code here. What free actually does for you is summarize memory utilization details coming from /proc/meminfo in a more "human way". So lets dissect this...
  • The main source code can be read here free.c, if you have a look at the lines between 359-377, you can see that there are 6 variables kb_main_total, kb_main_used, kb_main_free, kb_main_shared, kb_main_buffers and kb_main_cached and they are actually the ones containing the values that will show up in free's output. However if you look for these variables on the code you won't find them, they are extern variables included from "proc/sysinfo.h" which means the variables are being initialized somewhere else, particularly in "proc/sysinfo.c". The variables are actually being populated by meminfo() function being called in line 355.
  meminfo();
  /* Translation Hint: You can use 9 character words in
   * the header, and the words need to be right align to
   * beginning of a number. */
  if (flags & FREE_WIDE) {
   printf(_("              total        used        free      shared     buffers       cache   available"));
  } else {
   printf(_("              total        used        free      shared  buff/cache   available"));
  }
  printf("\n");
  printf("%-7s", _("Mem:"));
  printf(" %11s", scale_size(kb_main_total, flags, args));
  printf(" %11s", scale_size(kb_main_used, flags, args));
  printf(" %11s", scale_size(kb_main_free, flags, args));
  printf(" %11s", scale_size(kb_main_shared, flags, args));
  if (flags & FREE_WIDE) {
   printf(" %11s", scale_size(kb_main_buffers, flags, args));
   printf(" %11s", scale_size(kb_main_cached, flags, args));
  } else {
   printf(" %11s", scale_size(kb_main_buffers+kb_main_cached, flags, args));
  }
  printf(" %11s", scale_size(kb_main_available, flags, args));
  printf("\n");

  • Now checking the "proc/sysinfo.c" code where meminfo() function exists, we see:
    • kb_main_buffer is populated with the content of Buffers value from /proc/meminfo (line 691), this is memory used by the kernel to temporarily hold data being sent/received by the system (network IO, disk IO, etc).
    • kb_main_cached is populated with the result of "kb_page_cache + kb_slab_reclaimable" (line 762). These two values come from Cached and SReclaimable respectively (in /proc/meminfo). So the cached value presented by free contains the memory used by the page cache and the reclaimable slab memory (slab memory caches dentry and inodes structures to speed up some fs operations).
  {"Bounce",       &kb_bounce},
  {"Buffers",      &kb_main_buffers}, // important
  {"Cached",       &kb_page_cache},  // important
  {"CommitLimit",  &kb_commit_limit},
  ...
  kb_main_cached = kb_page_cache + kb_slab_reclaimable;
  kb_swap_used = kb_swap_total - kb_swap_free;

It is clear now that free relies completely on the details exposed by the kernel through /proc/meminfo, and the math involved in getting the values isn't really rocket science after all. But here it comes the interesting part...

To have in mind


It looks like the information provided by free can be slightly different between different Linux flavors (because they carry different free versions :D) so I thought it might be worth pointing that out here as well. This could lead to some inconsistencies when some other tools rely on the output of free.

Free version shipped with Ubuntu 14.04 (3.3.9), doesn't even include Slabs under the cached memory, you can see here how kb_main_cached value is just populated with Cached value from /proc/meminfo.

juan@test:~$ free -V
free from procps-ng 3.3.9
juan@test:~$ free
             total       used       free     shared    buffers     cached
Mem:       3007572     530948    2476624      14776      53080     244740
-/+ buffers/cache:     233128    2774444
Swap:            0          0          0
juan@test:~$ grep "^Cached\|^SReclaimable\|^Slab" /proc/meminfo
Cached:           244740 kB
Slab:              32308 kB
SReclaimable:      17572 kB
juan@test:~$

Note: cached = Cached

Free version shipped with CentOS 7 (3.3.10), defines kb_main_cached as "kb_page_cache + kb_slab" (706) this seems a minor thing but not all the slab memory is reclaimable therefore part of this cached content is not really available in case of memory pressure.

[juan@server ~]$ free -V
free from procps-ng 3.3.10
[juan@server ~]$ free -w
              total        used        free      shared     buffers       cache   available
Mem:        1016860       74212      818380        6688         948      123320      810600
Swap:             0           0           0
[juan@server ~]$ grep "^Cached\|^SRecl\|^Slab" /proc/meminfo
Cached:            88876 kB
Slab:              34444 kB
SReclaimable:      13928 kB
[juan@server ~]$

Note: cache = Cached + Slab

Interesting, isn't it? That's the power of open source after all right? Having the chance to really understand what certain piece of software is doing for you and how is doing it!

That's all about free, hope it helps (it helped me at least :P).

lunes, 8 de junio de 2015

UDP en GNU/Linux Parte II

Esta entrada es la continuación de UDP en GNU/Linux Parte I y la idea es continuar con las pruebas para entender un poco mas cómo funciona el stack UDP en GNU/Linux.

En la publicación anterior quedó pendiente probar packet receive errors  en una condición de buffer insuficiente. Dijimos que esta condición se puede dar en un escenario donde los paquetes arriban al host a una tasa mayor que con la que son retirados del buffer del socket. Para comprobar esto vamos a precisar algunos números y un receptor UDP lento.

Receptor UDP lento

La verdad es que no se me ocurrió ninguna idea maravillosa para hacer un receptor lento de paquetes UDP, así que hurgando un poco en internet armé un pequeño cliente.c (si ¬¬ .c, ¿qué tiene de malo?). El cliente básicamente realiza las siguientes tareas:
  • Abre un socket UDP y escucha en el puerto 5555.
  • Imprime en pantalla el tamaño del buffer del socket.
  • Luego en un loop toma del buffer del socket 1 paquete por segundo e imprime en pantalla un contador de paquetes leidos.
Acá pueden ver una ejecución abortada de ejemplo:
juan@ubuntu:~$ ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840
^C
juan@ubuntu:~$ 


Como ven el tamaño del buffer por defecto es de 163840 bytes (160Kbytes). Este valor no es aleatorio ni nada que se le parezca, el tamaño está definido por la variable de kernel net.core.rmem_default

root@ubuntu:/home/juan# sysctl -a|grep rmem_default
net.core.rmem_default = 163840
root@ubuntu:/home/juan#


por ejemplo, se podría ampliar de la siguiente manera

root@ubuntu:/home/juan# sysctl -w net.core.rmem_default=1638400
net.core.rmem_default = 1638400
root@ubuntu:/home/juan# sysctl -a|grep rmem_default
net.core.rmem_default = 1638400
root@ubuntu:/home/juan# 


y se vería reflejado en la próxima ejecución de nuestro cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 1638400
^C
root@ubuntu:/home/juan#  


Entonces, resumiendo los números:
  • Tamaño del buffer de recepción 163840 bytes.
  • Enviaremos paquetes de 1024 bytes forjados con Hping3. Por lo tanto, 160 paquetes son suficientes para llenar el buffer.
  • Se enviarán 2000 paquetes a una tasa de 10 paquetes por segundo.
  • La aplicación consume 1 paquete cada 1 segundo.
Está claro que si esta situación se prolonga lo suficiente en el tiempo, en algún momento el buffer se llenará y el sistema deberá descartar paquetes. Si bien es una condición un poco exagerada a fines prácticos resulta útil.

Antes de lanzar nuestro cliente lento tomamos los valores de las estadísticas del sistema:

root@ubuntu:/home/juan# netstat -su
Udp:
    20 packets received
    0 packets to unknown port received.
    0 packet receive errors
    22 packets sent
...
root@ubuntu:/home/juan#


lanzamos el cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840


y apuntamos el cañón hping3 al host:

root@moon:/home/juan/pruebas# hping3 192.168.0.10 -2 -c 2000 --fast -E 1024 -d 1024 -p 5555 -V
using wlan0, addr: 192.168.0.2, MTU: 1500
HPING 192.168.0.10 (wlan0 192.168.0.10): udp mode set, 28 headers + 1024 data bytes
[main] memlockall(): Success
Warning: can't disable memory paging!

--- 192.168.0.10 hping statistic ---
2000 packets transmitted, 0 packets received, 100% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
root@moon:/home/juan/pruebas#


esta vez hay nuevos parámetros en hping3:
  • --fast: le indica que debe mandar 10 paquetes por segundo.
  • --E 1024: indica que debe meter como payload de los paquetes el contenido del archivo llamado 1024 (un archivo con 1024 bytes en 0).
  • -d 1024: este parámetro es el que realmente indica cuántos bytes del archivo indicado por -E se van a mandar como payload. En esta caso se arman datagramas de 1024 bytes.
  • -c 2000: indica que se enviarán 2000 paquetes.
en definitiva se enviaron 2000 paquetes con 1024 bytes como payload cada uno de ellos (en total se enviaron unos 2048000 bytes, claramente superamos el tamaño del buffer).

Vemos la salida de la aplicación (dado que la función de lectura es bloqueante, la aplicación no termina por sus propios medios después de leer el último paquete, luego de unos segundos sin nuevos paquetes la aborté con Ctrl+C)

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840
Paquete numero 0
Paquete numero 1
Paquete numero 2
Paquete numero 3
Paquete numero 4
Paquete numero 5
Paquete numero 6
Paquete numero 7
...

Paquete numero 279
Paquete numero 280
Paquete numero 281
Paquete numero 282

^C
root@ubuntu:/home/juan#


Entonces se enviaron 2000 paquetes pero nuestra aplicación lenta solo pudo leer 283 de ellos!!! A donde fueron los restantes mmmm 1717 paquetes?

root@ubuntu:/home/juan# netstat -su
Udp:
    303 packets received
    0 packets to unknown port received.
    1717 packet receive errors
    22 packets sent
...
root@ubuntu:/home/juan#


Claramente fueron al cielo de los paquetes. Fueron completamente descartados por el kernel debido a que no había espacio donde almacenarlos.

Dado que leemos 1 paquete por segundo y agregamos 10 por segundo, podríamos decir que llegan un neto de 9 paquetes por segundo al buffer. A 9 paquetes por segundo el buffer se llena en unos 17 segundos aproximadamente. Es decir que, si la matemática no me falla, a partir de los 17 segundos estaríamos perdiendo al menos 9 paquetes por segundo.

Hay dos maneras de enfrentar esta situación, dado que UDP no tiene control de flujo e implementarlo en la capa de aplicación sería un delirio, podemos:
  • Incrementar la eficiencia del consumidor, leyendo mas paquetes por segundo.
  • Aumentando el tamaño del buffer de recepción para poder soportar el asedio de paquetes sin perderlos.
Entonces supongamos que mejoramos el código y multiplicamos por 7 la eficiencia de la aplicación y ahora es capaz de leer 7 paquetes por segundo. Matemáticamente estamos en la misma (en el horno), estarían ingresando un neto de 3 paquetes por segundo, ahora el buffer se llenaría en 53 segundos y volvemos a tener pérdida de paquetes. El paso siguiente es aumentar el tamaño del buffer. Sabemos que vamos a recibir una ráfaga de 2000 paquetes de 1024 bytes, osea 2Mbytes es decir que podríamos configurar un buffer de 2MBytes y quedarnos re tranquilos, pero sería algo extremo.

Con el nuevo código llenamos el buffer en 53 segundos, osea que aún nos quedan 147 segundos con pérdida de paquetes. Pero ahora vamos a ampliar el buffer multiplicándolo por 4 (NOTA: vale aclarar que este nuevo cambio se aplicará a todos los nuevos sockets creados).

root@ubuntu:/home/juan# echo "163840*4"|bc
655360
root@ubuntu:/home/juan# sysctl -w net.core.rmem_default=655360
net.core.rmem_default = 655360

root@ubuntu:/home/juan# sysctl -w net.core.rmem_max=655360
net.core.rmem_max = 655360

root@ubuntu:/home/juan# sysctl -a | grep net.core.rmem_default
net.core.rmem_default = 655360
root@ubuntu:/home/juan# 


En teoría, con este nuevo buffer de solo 4 veces mayor tamaño no deberíamos ver pérdida de paquetes.

root@ubuntu:/home/juan# netstat -su 
Udp:
    2072 packets received
    0 packets to unknown port received.
    0 packet receive errors
    21 packets sent
root@ubuntu:/home/juan#


Ejecución cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 655360
Paquete numero 7
Paquete numero 14
Paquete numero 21
Paquete numero 28
Paquete numero 35
Paquete numero 42
Paquete numero 49
Paquete numero 56

...
Paquete numero 1960
Paquete numero 1967
Paquete numero 1974
Paquete numero 1981
Paquete numero 1988
Paquete numero 1995
2000 paquetes leidos!!!

root@ubuntu:/home/juan#

Y corroboramos con las estadísticas:

root@ubuntu:/home/juan# netstat -su
Udp:
    4072 packets received
    0 packets to unknown port received.
    0 packet receive errors
    21 packets sent

root@ubuntu:/home/juan#

El aumento del tamaño del buffer y la mejora en la aplicación permitieron que esta vez no haya pérdida de paquetes.