Una comparativa de los núcleos Solaris, Linux y FreeBSD
Max Bruning,
14 de octubre de 2005
La mayoría de las clases que imparto están dedicadas a los aspectos internos de Solaris, drivers de dispositivos, y análisis y depurado de los volcados del núcleo tras un cuelgue. Al explicar cómo estan implementados en Solaris los diferentes subsistemas, los estudiantes preguntan con frecuencia, "¿Cómo funciona en Linux?" o, "¿FreeBSD lo hace de esta manera, cómo trabaja Solaris?". Este artículo examina tres subsistemas básicos del núcleo y compara la implementación de Solaris 10, Linux 2.6, y FreeBSD 5.3.
Los tres subsistemas examinados son el planificador, la gestión de memoria, y la arquitectura del sistema de ficheros. He escogido estos subsistemas porque son comunes a todos los sistemas operativos (no solo Unix o sistemas tipo Unix), y tienden a ser los componentes mejor entendidos del sistema operativo.
Este artículo no profundiza en detalles de ninguno de los subsistemas descritos. Para esto, hay que acudir al código fuente, varias páginas web, y libros sobre el tema. Algunos libros especificos son:
- Solaris Internals: Core Kernel Architecture por McDougall y Mauro (visite Solaris Internals)
- The Design and Implementation of the FreeBSD Operating System por McKusick y Neville-Neil (visite The Design and Implementation of the FreeBSD Operating System)
- Linux Kernel Development por Love (visite Linux Kernel Development, 2nd Edition) y Understanding the Linux Kernel por Bovet y Cesati (visite Understanding the Linux Kernel, 2nd Edition)
Al buscar en la Web comparativas de Linux, FreeBSD y Solaris, la mayoría de los resultados tratan sobre versiones antiguas de los sistemas operativos (en algunos casos, Solaris 2.5, Linux 2.2, etc...). Muchas de las "afirmaciones" son incorrectas en las nuevas versiones y algunas son incorrectas para las versiones que intentan describir. Por supuesto, la mayoría de ellas también hacen juicios de valor sobre los méritos de los sistemas operativos, y aportan poca información comparando los propios núcleos. Las siguientes páginas parecen estar más o menos actualizadas:
- "Solaris Vs. Linux" is pretty one-sided for Solaris 10 over Linux.
- "Comparing MySQL Performance" sobre Solaris 10, Linux, FreeBSD y otros.
- "Fast Track to Solaris 10 Adoption" tiene algunas comparativas entre Linux y Solaris.
- "Solaris 10 Heads for Linux Territory" no es realmente una comparativa, pero describe Solaris 10.
Uno de los aspectos más interesantes de los tres sistemas operativos es la cantidad de similitudes entre ellos. Una vez que se han superado las diferentes nomenclaturas, cada SO recorre caminos muy similares en la implementación de los diferentes conceptos. Cada SO soporta planificación de tiempo compartido para hebras, paginación con un algoritmo de reemplazo página no usada recientemente (NRU, Not Recently Used), y una capa de sistema de ficheros virtual para permitir la implementación de diferentes sistemas de ficheros. Las ideas que nacen en un SO con frecuencia acaban llegando a los otros. Por ejemplo, Linux utiliza conceptos que están detrás del slab memory allocator de Solaris. Mucha de la terminologia vista en el codigo fuente de FreeBSD también está presente en Solaris. Como Sun está abriendo el código fuente de Solaris, espero ver un mayor mestizaje de caracteristicas. Actualmente, el pryecto LXR proporciona una fuente de referencias cruzadas para FreeBSD, Linux y otros SO tipo Unix, disponible en fxr.watson.org. Será magnífico ver el código fuente de OpenSolaris añadido a este sitio1.
Planificando y Planificadores
La unidad básica de planificación en Solaris es kthread_t
; en FreeBSD thread
; y en Linux task_struct
. Solaris representa cada proceso como un proc_t
, y cada hebra dentro de un proceso tiene una kthread_t
. Linux representa procesos (y hebras) con estructuras task_struct
. Un proceso con una sola hebra en Linux tiene una sola task_struct
. Un proceso con una sola hebra en Solaris tiene proc_t
, una sola kthread_t
, y una klwp_t
. La estructura klwp_t
proporciona un área de almacenamiento para el intercambio de hebras entre los modos de usuario y núcleo. Un proceso con una sola hebra en FreeBSD tiene una estructura proc
, una estructura thread
, y una estructura ksegrp
. El ksegrp
es un "grupo de entidades planificables por el núcleo" (kernel scheduling entity group). Realmente, los tres SOs planifican hebras, y una hebra es una kthread_t
en Solaris, una estructura thread
en FreeBSD, y una task_struct
en Linux.
Las decisiones de planificación están basadas en la prioridad. En Linux y FreeBSD, los valores bajos de prioridad son mejores. Una prioridad próxima a 0 representa una alta prioridad. En Solaris, a más valor prioridad más alta. La Tabla 1 muestra los valores de prioridad en diferentes SOs.
Solaris | ||
Priorities | Scheduling Class | |
---|---|---|
0-59 | Time Shared, Interactive, Fixed, Fair Share Scheduler | |
60-99 | System Class | |
100-159 | Real-Time (note real-time higher than system threads) | |
160-169 | Low level Interrupts | |
Linux | Priorities | Scheduling Class |
0-99 | System Threads, Real time (SCHED_FIFO , SCHED_RR ) |
|
100-139 | User priorities (SCHED_NORMAL ) |
|
FreeBSD | Priorities | Scheduling Class |
0-63 | Interrupt | |
64-127 | Top-half Kernel | |
128-159 | Real-time user (system threads are better priority) | |
160-223 | Time-share user | |
224-255 | Idle user |
Los tres SOs favorecen los procesos/hebras interactivos. Las hebras interactivas se ejecutan con mejor prioridad que las hebras con trabajos intensivos para la CPU, pero tienden a ejecutarse durante franjas de tiempo más pequeñas. Solaris, FreeBSD, y Linux utilizan una cola de ejecución por CPU. FreeBSD y Linux utilizan una cola "activa" y una cola "expirados". Las hebras se planifican con prioridad desde la cola activa. Una hebra pasa de la cola activa a la cola expirados cuando consume la franja de tiempo asignada (y posiblemente en otros momento para evitar la inanición). Cuando la cola activa está vacía, el kernel intercambia la cola activa y la de procesos expirados. FreeBSD tiene una tercera cola para las hebras "idle". Las hebras se ejecutan en esta cola sólo cuando las otras dos colas están vacias. Solaris utiliza una cola de envío por CPU. Si una hebra consume su franja de tiempo, el núcleo le asigna una nueva prioridad y la devuelve a la cola de envío. Las colas de ejecución para los tres SOs almacenan las hebras ejecutables en diferentes listas enlazadas, una por prioridad. (Aunque FreeBSD utiliza una lista para cuatro prioridades, tanto Solaris como Linux utilizan una lista separada por cada prioridad.)
Linux y FreeBSD utilizan un cálculo artimético basado en el tiempo de ejecución versus el tiempo inactivo de la hebra (como una medida de la "interactividad") para obtener la prioridad de la hebra. Solaris realiza una búsqueda en una tabla. Ninguno de los tres SOs soporta "gang scheduling"2. En lugar de planificar n hebras, cada SO planifica, la próxima hebra a ejecutar. Los tres SOs tienen mecanismos para sacar partido de la cache (afinidad) y balanceo de carga. En las CPUs con hyperthreading3 FreeBSD incorpora un mecanismo que ayuda a mantener hebras en la misma CPU. Solaris tiene un mecanismo similar, pero está bajo control del usuario y la aplicación, y no está restringido a hyperthreads (llamados "conjuntos de procesadores" en Solaris y "grupos de procesadores" en FreeBSD).
Una gran diferencia entre Solaris y los otros dos SOs es la capacidad para soportar "clases de planificación" de forma simultánea en el sistema. Los tres SOs soportan Posix SCHED_FIFO
, SCHED_RR
, y SCHED_OTHER
(o SCHED_NORMAL
). SCHED_FIFO
y SCHED_RR
normalmente se utilizan en las hebras "tiempo real". (Tanto Solaris como Linux soportan apropiación en el núcleo3 para asistir a las hebras de tiempo real.) Solaris tiene soporte para una clase "prioridad fija", "clase de sistema" para las hebras de sistema (como las hebras de paginación), una clase "interactiva" utilizada por hebras que se ejecutan en un entorno de ventanas bajo el control del servidor X, y el planificador de reparto limpio (Fair Share Scheduler) para asistir en la gestión de recursos. Consulte priocntl(1)
para obtener información sobre el uso de estas clases y una visión general de las características de cada clase. Consulte FSS(7)
para obtener información específica del Fair Share Scheduler. El planificador de FreeBSD se escoge en tiempo de compilación, y en Linux el planificador depende de la versión de Linux.
La habilidad de añadir nuevas clases de planificación al sistema tiene un precio. En todos los lugares del núcleo en los que se puede tomar una decisión de planificación (excepto para escoger la hebra a ejecutar) se realiza una llamada indirecta a código específico de la clase de planificador. Por ejemplo, cuando una hebra se va a dormir, llama a código dependiente de la clase de planificador que realiza la acción necesaria para dormir en la clase. En Linux y FreeBSD, el código de planificación símplemente realiza la acción necesaria. No hay necesidad de una llamada indirecta. La capa extra significa un pequeño aumento de la sobrecarga de planificación en Solaris (pero se obtienen más características).
Gestión de la memoria y Paginación
En Solaris cada proceso tiene un "espacio de direcciones" formado por secciones lógicas llamadas "segmentos". Los segmentos del espacio de direcciones de un proceso se pueden ver a través de pmap(1)
. Solaris divide el código de gestión de memoria y las estructuras de datos en la parte independiente y dependiente de la plataforma. La porción específica de la plataforma de la gestión de memoria se encuentra en la capa HAT (hardware address translation) o traducción de direcciones físicas. FreeBSD describe el espacio de direcciones de un proceso mediante un vmspace, dividido en secciones lógicas llamadas regiones. Las porciones dependientes del hardware están en el módulo "pmap" (physical map) mapa físico y las rutinas "vmap" manejan las porciones y estructuras de datos independientes del hardware. Linux utiliza un descriptor de memoria para dividir el espacio de direcciones de un proceso en secciones lógicas llamadas "áreas de memoria" que describen el espacio de direcciones del proceso. Linux también tiene un comando pmap
para examinar el espacio de direcciones de un proceso.
Linux divides machine-dependent layers from machine-independent layers at a much higher level in the software. On Solaris and FreeBSD, much of the code dealing with, for instance, page fault handling is machine-independent. On Linux, the code to handle page faults is pretty much machine-dependent from the beginning of the fault handling. A consequence of this is that Linux can handle much of the paging code more quickly because there is less data abstraction (layering) in the code. However, the cost is that a change in the underlying hardware or model requires more changes to the code. Solaris and FreeBSD isolate such changes to the HAT and pmap layers respectively.
Segments, regions, and memory areas are delimited by:
- Virtual address of the start of the area.
- Their location within an object/file that the segment/region/memory area maps.
- Permissions.
- Size of the mapping.
For instance, the text of a program is in a segment/region/memory area. The mechanisms in the three OSes to manage address spaces are very similar, but the names of data structures are completely different. Again, more of the Linux code is machine-dependent than is true of the other two OSes.
Paging
All three operating systems use a variation of a least recently used algorithm for page stealing/replacement. All three have a daemon process/thread to do page replacement. On FreeBSD, the vm_pageout
daemon wakes up periodically and when free memory becomes low. When available memory goes below some thresholds, vm_pageout
runs a routine (vm_pageout_scan
) to scan memory to try to free some pages. The vm_pageout_scan
routine may need to write modified pages asynchronously to disk before freeing them. There is one of these daemons regardless of number of CPUs. Solaris also has a pageout
daemon that also runs periodically and in response to low-free-memory situations. Paging thresholds in Solaris are automatically calibrated at system startup so that the daemon does not overuse the CPU or flood the disk with page-out requests. The FreeBSD daemon uses values that, for the most part, are hard-coded or tunable in order to determine paging thresholds. Linux also uses an LRU algorithm that is dynamically tuned while it runs. On Linux, there can be multiple kswapd
daemons, as many as one per CPU. All three OSes use a global working set policy (as opposed to per process working set).
FreeBSD has several page lists for keeping track of recently used pages. These track "active," "inactive," "cached," and "free" pages. Pages move between these linked lists depending on their uses. Frequently accessed pages will tend to stay on the active list. Data pages of a process that exits can be immediately placed on the free list. FreeBSD may swap entire processes out if vm_pageout_scan
cannot keep up with load (for example, if the system is low on memory). If the memory shortage is severe enough, vm_pageout_scan
will kill the largest process on the system.
Linux also uses different linked lists of pages to facilitate an LRU-style algorithm. Linux divides physical memory into (possibly multiple sets of) three "zones:" one for DMA pages, one for normal pages, and one for dynamically allocated memory. These zones seem to be very much an implementation detail caused by x86 architectural constraints. Pages move between "hot," "cold," and "free" lists. Movement between the lists is very similar to the mechanism on FreeBSD. Frequently accessed pages will be on the "hot" list. Free pages will be on the "cold" or "free" list.
Solaris uses a free list, hashed list, and vnode page list to maintain its variation of an LRU replacement algorithm. Instead of scanning the vnode or hash page lists (more or less the equivalent of the "active"/"hot" lists in the FreeBSD/Linux implementations), Solaris scans all pages uses a "two-handed clock" algorithm as described in Solaris Internals and elsewhere. The two hands stay a fixed distance apart. The front hand ages the page by clearing reference bit(s) for the page. If no process has referenced the page since the front hand visited the page, the back hand will free the page (first asynchronously writing the page to disk if it is modified).
All three operating systems take NUMA locality into account during paging. The I/O buffer cache and the virtual memory page cache is merged into one system page cache on all three OSes. The system page cache is used for reads/writes of files as well as mmap
ped files and text and data of applications.
File Systems
All three operating systems use a data abstraction layer to hide file system implementation details from applications. In all three OSes, you use open
, close
, read
, write
, stat
, etc. system calls to access files, regardless of the underlying implementation and organization of file data. Solaris and FreeBSD call this mechanism VFS ("virtual file system") and the principle data structure is the vnode
, or "virtual node." Every file being accessed in Solaris or FreeBSD has a vnode
assigned to it. In addition to generic file information, the vnode
contains pointers to file-system-specific information. Linux also uses a similar mechanism, also called VFS (for "virtual file switch"). In Linux, the file-system-independent data structure is an inode
. This structure is similar to the vnode
on Solaris/FreeBSD. (Note that there is an inode
structure in Solaris/FreeBSD, but this is file-system-dependent data for UFS file systems). Linux has two different structures, one for file operations and the other for inode operations. Solaris and FreeBSD combine these as "vnode operations."
VFS allows the implementation of many file system types on the system. This means that there is no reason that one of these operating systems could not access the file systems of the other OSes. Of course, this requires the relevant file system routines and data structures to be ported to the VFS of the OS in question. All three OSes allow the stacking of file systems. Table 2 lists file system types implemented in each OS, but it does not show all file system types.
Solaris | ufs | Default local file system (based on BSD Fast Filesystem) |
nfs | Remote Files | |
proc | /proc files; see proc(4) |
|
namefs | Name file system; allows opening of doors/streams as files | |
ctfs | Contract file system used with Service Management Facility | |
tmpfs | Uses anonymous space (memory/swap) for temporary files | |
swapfs | Keeps track of anonymous space (data, heap, stack, etc.) | |
objfs | Keeps track of kernel modules, see objfs(7FS) |
|
devfs | Keeps track of /devices files; see devfs(7FS) |
|
FreeBSD | ufs | Default local file system (ufs2, based on BSD Fast Filesystem) |
defvs | Keeps track of /dev files | |
ext2 | Linux ext2 file system (GNU-based) | |
nfs | Remote files | |
ntfs | Windows NT file system | |
smbfs | Samba file system | |
portalfs | Mount a process onto a directory | |
kernfs | Files containing various system information | |
Linux | ext3 | Journaling, extent-based file system from ext2 |
ext2 | Extent-based file system | |
afs | AFS client support for remote file sharing | |
nfs | Remote files | |
coda | Another networked file system | |
procfs | Processes, processors, buses, platform specifics | |
reiserfs | Journaling file system |
Conclusions
Solaris, FreeBSD, and Linux are obviously benefiting from each other. With Solaris going open source, I expect this to continue at a faster rate. My impression is that change is most rapid in Linux. The benefits of this are that new technology has a quick incorporation into the system. Unfortunately, the documentation (and possibly some robustness) sometimes lags behind. Linux has many developers, and sometimes it shows. FreeBSD has been around (in some sense) the longest of the three systems. Solaris has its basis in a combination of BSD Unix and AT&T Bell Labs Unix. Solaris uses more data abstraction layering, and generally could support additional features quite easily because of this. However, most of the layering in the kernel is undocumented. Probably, source code access will change this.
A brief example to highlight differences is page fault handling. In Solaris, when a page fault occurs, the code starts in a platform-specific trap handler, then calls a generic as_fault()
routine. This routine determines the segment where the fault occurred and calls a "segment driver" to handle the fault. The segment driver calls into file system code. The file system code calls into the device driver to bring in the page. When the page-in is complete, the segment driver calls the HAT layer to update page table entries (or their equivalent). On Linux, when a page fault occurs, the kernel calls the code to handle the fault. You are immediately into platform-specific code. This means the fault handling code can be quicker in Linux, but the Linux code may not be as easily extensible or ported.
Kernel visibility and debugging tools are critical to get a correct understanding of system behavior. Yes, you can read the source code, but I maintain that you can easily misread the code. Having tools available to test your hypothesis about how the code works is invaluable. In this respect, I see Solaris with kmdb
, mdb
, and DTrace as a clear winner. I have been "reverse engineering" Solaris for years. I find that I can usually answer a question by using the tools faster than I can answer the same question by reading source code. With Linux, I don't have as much choice for this. FreeBSD allows use of gdb
on kernel crash dumps. gdb
can set breakpoints, single step, and examine and modify data and code. On Linux, this is also possible once you download and install the tools.
Max Bruning currently teaches and consults on Solaris internals, device drivers, kernel (as well as application) crash analysis and debugging, networking internals, and specialized topics. Contact him at max at bruningsystems dot com or http://mbruning.blogspot.com/.
Notas de traducción:
- El proyecto LXR ya incluye el código fuente de OpenSolaris: http://fxr.watson.org/fxr/source/?v=OPENSOLARIS
- Gang scheduling: Se planifican hebras o procesos relacionados para que se ejecuten de forma simultánea en diferentes procesadores. http://en.wikipedia.org/wiki/Gang_scheduling
- kernel preemptition
- Hyperthreading, tecnología que proporciona soporte para ejecutar varias hebras de forma simultánea en el procesador. http://es.wikipedia.org/wiki/HyperThreading