Creación de daemons SysV en C

Vamos a ver la creación de daemons (demonios) que son como se denominan los servicios en GNU/Linux, siguiendo la forma tradicional de los daemons SysV. Para la creación seguiremos los 15 pasos que se detallan en man 7 daemon.

El daemon es un programa que tienen unas características especiales, entre las que podemos destacar:

  • Suelen tener un tiempo de ejecución prolongado. Con frecuencia arrancan y paran con el sistema
  • No tienen un terminal controlador, es decir, no son interactivos, por tanto cualquier salida a stderr o stdout requiere un manipulador especial
  • Suelen ejecutarse con privilegios root, debido a la necesidad de algún tipo de acceso a recurso que requiere ese privilegio

Aunque existen muchas formas de crear un daemon, vamos a seguir los pasos que se detallan en man 7 daemon para su creación. El objetivo del código es ser lo más claro posible, mostrando cada paso e indicando por pantalla o syslog los pasos que se van dando.

El código pretende ser didáctico, por lo que he evitado expresamente macros o llamadas a subrutinas, es excesivamente verboso y la gestión de errores poco elaborada. Esto hace el código poco vistoso, pero creo que más legible.

15 pasos para crear un daemon SysV

1. Cierre todos los descriptores de archivo abiertos, excepto los estándar de entrada, salida y error (es decir, los primeros tres descriptores de archivo 0, 1, 2). Esto garantiza que ningún descriptor de archivo pasado accidentalmente permanezca en el proceso del daemon. En Linux, esto se implementa mejor iterando a través de /proc/self/fd, con un retroceso de iteración desde el descriptor de archivo 3 al valor devuelto por getrlimit () para RLIMIT_NOFILE

Usamos prlimit para obtener el descriptor máximo y los cerramos todos en un bucle for:

/* 1. Cerramos todos los descriptores de fichero menos 0,1,2 */  
if (prlimit(0, RLIMIT_NOFILE, 0, &rlim) < 0) { 
    printf("prlimit RLIMIT_NOFILE failed\n"); 
    exit(EXIT_FAILURE); 
} 
printf("%d:%s: [+] Paso 1 : Cerramos fd's (3 a %ld)\n", 
    getpid(), __func__, rlim.rlim_max); 
for (i = 3; i < (int) rlim.rlim_max; i++) 
    (void)close(i);  

2. Restablecer todos los controladores de señal a sus valores predeterminados. Esto se hace mejor mediante la iteración a través de las señales disponibles hasta el límite de _NSIG y restableciéndolas a SIG_DFL

Reseteamos los manejadores de señal a SIG_DFL iterando hasta _NSIG, excepto las señales no enmascarables:

/* 2. Resetear todos los manejadores de señales a sus valores por defecto */ 
printf("%d:%s: [+] Paso 2 : Reseteamos los manejadore se señales (1 a %d)\n", 
    getpid(), __func__, _NSIG); 
memset(&sa, 0, sizeof(sa)); 
sa.sa_handler = SIG_DFL; 
for (i = 1; i < _NSIG; i++) { 
    if ((i == SIGKILL) || (i == SIGSTOP) ||  
         (i == 32) || (i == 33)) 
        continue; // evitamos las señales no enmascarables y NTPL 
    if (sigaction(i, &sa, NULL) == -1) { 
        printf("sigaction %d falló\n", i); 
        exit(EXIT_FAILURE); 
    } 
} 

3. Restablecer la máscara de señal utilizando sigprocmask()

/* 3. Restablecer la máscara de señal utilizando sigprocmask() */ 
printf("%d:%s: [+] Paso 3 : Reseteamos signal mask\n",  
 getpid(), __func__); 
if ((sigfillset(&sa.sa_mask) < 0) ||  
 (sigprocmask(SIG_UNBLOCK, &sa.sa_mask, NULL) < 0)) { 
 printf("Fallo en sigfillset/sigprocmask\n"); 
 exit(EXIT_FAILURE); 
} 

4. Sanear el bloque de entorno, elimine o restablezca las variables de entorno que podrían afectar negativamente al tiempo de ejecución del daemon

En este caso borramos todas las variables de entorno. Si fuera necesario posteriormente crearíamos las necesarias:

/* 4. Sanear el bloque de entorno */ 
printf("%d:%s: [+] Paso 4 : Borramos las variables de entorno\n",  
    getpid(), __func__); 
/* Borramos todos las variables de entorno */ 
clearenv(); 

5. Llamar a fork() para crear un proceso en segundo plano

/* 5. Llamar a fork() para crear un proceso en segundo plano */ 
printf("%d:%s: [+] Paso 5 : Primer fork\n", getpid(), __func__); 
 
switch (fork()) { 
    case -1: // Error en fork() 
        printf("fork #1 falló\n"); 
        close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE); 
 
    case 0: // Primer child 
        close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
        break; // salimos del switch y continuamos  

6. En el proceso hijo, llame a setsid() para desconectarse de cualquier terminal y crear una sesión independiente

/* 6. En el proceso hijo, llame a setsid() */ 
printf("%d:%s: [+] Paso 6 : Creamos una nueva sesión\n", 
    getpid(), __func__); 
if (setsid() < 0) { 
    printf("Fallo en setsid\n"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_FAILURE); 
} 

7. En el proceso hijo, vuelva a llamar a fork() para asegurarse de que el daemon nunca pueda volver a adquirir un terminal de nuevo

/* 7. En el proceso hijo, vuelva a llamar a fork() */ 
printf("%d:%s: [+] Paso 7 : Segundo fork\n", getpid(), __func__); 
pid = fork(); 
 
/* El pid menor que 0 es un error */ 
if (pid < 0) { 
    printf("fork #2 falló\n"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_FAILURE); 
} 

8. Llame a exit () en el primer proceso hijo, de modo que solo quede el segundo proceso hijo (el proceso del daemon real). Esto asegura que el proceso del daemon desciende del proceso init/PID 1, que es como debe ser en todos los daemons

if (pid > 0) { 
    /* 8. Llame a exit () en el primer proceso hijo */ 
    printf("%d:%s: [+] Paso 8 : Primer proceso hijo finalizado\n", 
        getpid(), __func__); 
 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_SUCCESS); 
} 

9. En el proceso del daemon, conecte la entrada, salida y error estándar a /dev/null

/* 9. En el proceso del daemon, conecte entrada/salida/error a /dev/null */ 
printf("%d:%s: [+] Paso 9 : Conectar los fd 0,1,2 a dev null\n", 
    getpid(), __func__); 
 
fd = open("/dev/null", O_RDWR); 
if (fd < 0) { 
    printf("Fallo al abrir dev null\n"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_FAILURE); 
} 
 
printf("--------- Pasos [10][11][12][13] o errores en syslog\n"); 
 
if (dup2(fd, STDIN_FILENO) < 0 || 
     dup2(fd, STDOUT_FILENO) < 0 || 
     dup2(fd, STDERR_FILENO) < 0) {  
    syslog (LOG_ERR, "Fallo en dup2 STDIN/OUT/ERR_FILENO"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe 
    close(fd); 
    exit(EXIT_FAILURE);  
} 
 
close(fd); 

10. En el proceso del daemon, restablezca umask a 0, de modo que los modos de archivo pasados a open(), mkdir() y similares controlen directamente el modo de acceso de los archivos y directorios creados

/* 10. En el proceso del daemon, restablezca umask a 0 */ 
syslog (LOG_NOTICE, "[+] Paso 10 : Reseteamos umask a 0"); 
umask(0x0); 

11. En el proceso del daemon, cambie el directorio actual al directorio raíz (/), para evitar que el daemon bloquee involuntariamente el desmontaje de los puntos de montaje

/* 11. Cambie el directorio actual al directorio raíz (/) */ 
syslog (LOG_NOTICE, "[+] Paso 11 : Cambiamos el directorio a '/'"); 
if (chdir("/") < 0) { 
    syslog (LOG_ERR, "Fallo en chdir a '/'"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_FAILURE);  
} 

12. En el proceso del daemon, escriba el PID del demonio (como lo devuelve getpid()) en un archivo PID, por ejemplo /run/foobar.pid (para un demonio hipotético "foobar") para asegurarse de que el demonio no pueda iniciarse más de una vez. Esto debe implementarse de forma race-free para que el archivo PID solo se actualice cuando se verifique al mismo tiempo que el PID previamente almacenado en el archivo PID ya no exista o pertenezca a un proceso externo

/* 12. Daemon: escriba el PID del daemon en un archivo PID */ 
/* Abrimos o creamos el fichero para grabar el PID,  
    si no podemos bloquearlo finalizamos,  
    probablemente el demonio ya está en ejecución. */ 
syslog (LOG_NOTICE, "[+] Paso 12 : Grabamos PID"); 
 
pid_fd = open(pid_file_name, O_RDWR|O_CREAT, 0640); 
if (pid_fd < 0) { 
    /* No podemos habrir el fichero */ 
    syslog (LOG_ERR, "No se puede crear o abrir el fichero %s\n", pid_file_name); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    exit(EXIT_FAILURE); 
} 
if (lockf(pid_fd, F_TLOCK, 0) < 0) { 
    /* No podemos bloquear el fichero */ 
    syslog (LOG_ERR, "No se puede bloquear el fichero %s\n", pid_file_name); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    close(pid_fd); // Cerramos el fichero para grabar el PID  
    exit(EXIT_FAILURE); 
} 
 
snprintf(str, sizeof(str), "%d\n", getpid()); 
/* Escribimos el PID en el fichero */ 
if (write(pid_fd, str, strlen(str)) == -1) { 
    syslog (LOG_ERR, "Error al escribir el pid en %s\n", pid_file_name); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    close(pid_fd); // Cerramos el fichero para grabar el PID  
    exit(EXIT_FAILURE);  
} 

13. En el proceso del daemon, elimine los privilegios innecesarios, si es posible y aplicable

En este ejemplo arrancamos el servicio como root y en este paso lo pasamos a dgroupID y duserID, que son usuarios normales:

/* 13. Daemon: elimine los privilegios innecesarios */ 
syslog (LOG_NOTICE, "[+] Paso 13 : Reducimos privilegios"); 
 
if (setgid (dgroupID) == -1) { 
    syslog (LOG_ERR, "Error en setgid"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    close(pid_fd); // Cerramos el fichero para grabar el PID  
    exit(EXIT_FAILURE);  
} 
 
if (setuid (duserID) == -1) { 
    syslog (LOG_ERR, "Error en setuid"); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
    close(pid_fd); // Cerramos el fichero para grabar el PID  
    exit(EXIT_FAILURE);  
} 
 
if (getresuid(&ruid, &euid, &suid) == -1) 
    syslog (LOG_ERR, "Error en getresuid"); 
else 
    syslog (LOG_NOTICE, "UID: (%ld) (%ld) (%ld)\n", 
        (long) ruid, (long) euid, (long) suid); 
 
if (getresgid(&rgid, &egid, &sgid) == -1) 
    syslog (LOG_ERR, "Error en getresgid"); 
else 
    syslog (LOG_NOTICE, "GID: (%ld) (%ld) (%ld)\n", 
        (long) rgid, (long) egid, (long) sgid); 

14. Desde el proceso del daemon, notifique al proceso original primario que la inicialización está completa. Esto se puede implementar a través de un conducto sin nombre (unnamed pipe) o un canal de comunicación similar que se crea antes del primer fork() y por lo tanto, está disponible tanto en el proceso original como en el del daemon

Hemos creado un conducto sin nombre llamado pipefd (ver el código completo más abajo) que usamos para notificar la iniciación correcta:

/* 14. Daemon: notificamos que la inicialización está completa */ 
snprintf(mes_pipe, sizeof(mes_pipe),  
    "%d:%s: [+] Paso 14 : Nofiticamos al proceso original (por pipe)" 
    "\n\t\t\t (daemon creado correctamente)",  
    getpid(), __func__); 
 
write(pipefd[1], mes_pipe, strlen(mes_pipe)); 
close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  

15. Llame a exit() en el proceso original. El proceso que invocó al demonio debe poder confiar en que este exit() ocurre después de que se completa la inicialización y se establecen y se puede acceder a todos los canales de comunicación externos

/* 15. Llame a exit() en el proceso original */ 
printf("%d:%s: [+] paso 15 : Finalizando el proceso padre primario\n", 
    getpid(), __func__); 
exit(EXIT_SUCCESS); 

Daemon de ejemplo

Este es el codigo completo de la aplicación de ejemplo. El daemon incluye una funcionalidad básica que es esperar en el puerto 1973 y hacer un eco de lo que se reciba incluyendo el nombre del daemon y el PID:

/* 
 * daemon_sysv.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
 * 
 *  
 *  
 **************************************************************** 
 * Descripción: 
 * 
 * Codigo de ejemplo para crear un daemon SysV siguiendo los  
 * 15 pasos que se indican en daemon(7) 
 * 
 * --------  
 * 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <errno.h> 
#include <signal.h> 
#include <string.h> 
#include <sys/time.h> 
#include <sys/types.h> 
#include <sys/resource.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <syslog.h> 
#include <getopt.h> 
#include <sys/ioctl.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
 
static const char *pid_file_name = "/run/daemon_sysv.pid"; 
static const uid_t duserID = 1000; /* ID de usuario para el daemon */ 
static const gid_t dgroupID = 1000; /* ID de grupo para el daemon */ 
int pid_fd=-1; /* descriptor del fichero pid_file_name */ 
 
static volatile sig_atomic_t SignalSalir = 0; /* Señal de salir */ 
static volatile sig_atomic_t SignalReload = 0; /* Señal de recargar */ 
 
void handle_signal(int sig) 
{ 
    if (sig == SIGINT) { 
        SignalSalir = 1; 
    } else if (sig == SIGHUP) { 
        SignalReload = 1; 
    }  
} 
 
static void daemonize() 
{ 
    pid_t pid=-1; 
    int fd=-1; 
    char str[256]; 
    int i=0; 
    struct rlimit rlim; 
    struct sigaction sa; 
    int pipefd[2] = {-1, -1}; 
    char buf; 
    char mes_pipe[256]; 
    ssize_t pipenumRead; 
    uid_t ruid, euid, suid; 
    gid_t rgid, egid, sgid; 
         
    /* 1. Cerramos todos los descriptores de fichero abiertos menos 0,1,2 */  
    if (prlimit(0, RLIMIT_NOFILE, 0, &rlim) < 0) { 
        printf("Fallo en prlimit RLIMIT_NOFILE\n"); 
        exit(EXIT_FAILURE); 
    } 
    printf("%d:%s: [+] Paso 1 : Cerramos fd's (3 a %ld)\n", 
        getpid(), __func__, rlim.rlim_max); 
    for (i = 3; i < (int) rlim.rlim_max; i++) 
        (void)close(i); 
 
    /* 2. Resetear todos los manejadores de señales a sus valores por defecto */ 
    printf("%d:%s: [+] Paso 2 : Reseteamos los manejadore se señales (1 a %d)\n", 
        getpid(), __func__, SIGRTMAX); 
    memset(&sa, 0, sizeof(sa)); 
    sa.sa_handler = SIG_DFL; 
    for (i = 1; i < SIGRTMAX; i++) { 
        if ((i == SIGKILL) || (i == SIGSTOP) ||  
             (i == 32) || (i == 33)) 
            continue; // evitamos las señales no enmascarables y NTPL 
        if (sigaction(i, &sa, NULL) == -1) { 
            printf("sigaction %d falló\n", i); 
            exit(EXIT_FAILURE); 
        } 
    } 
 
    /* 3. Restablecer la máscara de señal utilizando sigprocmask() */ 
    printf("%d:%s: [+] Paso 3 : Reseteamos signal mask\n",  
        getpid(), __func__); 
    if ((sigfillset(&sa.sa_mask) < 0) ||  
        (sigprocmask(SIG_UNBLOCK, &sa.sa_mask, NULL) < 0)) { 
        printf("Fallo en sigfillset/sigprocmask\n"); 
        exit(EXIT_FAILURE); 
    } 
 
    /* 4. Sanear el bloque de entorno */ 
    printf("%d:%s: [+] Paso 4 : Borramos las variables de entorno\n",  
        getpid(), __func__); 
    /* Borramos todos las variables de entorno */ 
    clearenv(); 
 
    /* Abrimos un pipe sin nombre para comunicar con el daemon */ 
    if (pipe(pipefd) == -1) { 
        printf("pipe falló\n"); 
        exit(EXIT_FAILURE); 
    } 
 
    /* 5. Llamar a fork() para crear un proceso en segundo plano */ 
    printf("%d:%s: [+] Paso 5 : Primer fork\n", getpid(), __func__); 
 
    switch (fork()) { 
        case -1: // Error en fork() 
            printf("fork #1 falló\n"); 
            close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
            close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
            exit(EXIT_FAILURE); 
 
        case 0: // Primer child 
            close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
            break; // salimos del switch y continuamos  
 
        default: // Proceso padre  
            close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
 
            for(i=0;;i++) { // Esperamos la comunicación desde el daemon  
                pipenumRead = read(pipefd[0], &buf, 1); 
                if(pipenumRead == -1) { 
                    printf("Fallo en lectura del pipe\n"); 
                    close(pipefd[0]); // Cerramos el descriptor de lectura del pipe 
                    exit(EXIT_FAILURE); 
                }  
                if(pipenumRead == 0) 
                    break; 
                      
                write(STDOUT_FILENO, &buf, 1);  
            } 
 
            if (i>0) { // hemos recibido comunicación del daemon  
                write(STDOUT_FILENO, "\n", 1); 
                close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
            } else { // no recibimos nada, algo no funcionó  
                snprintf(mes_pipe, sizeof(mes_pipe),  
                    "%d:%s: Se han producido errores al iniciar el daemon "  
                    "(ver syslog)\n", getpid(), __func__); 
                write(STDOUT_FILENO, mes_pipe, strlen(mes_pipe)); 
                close(pipefd[0]); // Cerramos el descriptor de lectura del pipe  
                exit(EXIT_FAILURE); 
            } 
 
            /* 15. Llame a exit() en el proceso original */ 
            printf("%d:%s: [+] paso 15 : Finalizando el proceso padre primario\n", 
                getpid(), __func__); 
            exit(EXIT_SUCCESS); 
    } // primer fork 
 
    /* 6. En el proceso hijo, llame a setsid() */ 
    printf("%d:%s: [+] Paso 6 : Creamos una nueva sesión\n", 
        getpid(), __func__); 
    if (setsid() < 0) { 
        printf("Fallo en setsid\n"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE); 
    } 
 
    /* 7. En el proceso hijo, vuelva a llamar a fork() */ 
    printf("%d:%s: [+] Paso 7 : Segundo fork\n", getpid(), __func__); 
    pid = fork(); 
 
    /* El pid menor que 0 es un error */ 
    if (pid < 0) { 
        printf("fork #2 falló\n"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE); 
    } 
 
    if (pid > 0) { 
        /* 8. Llame a exit () en el primer proceso hijo */ 
        printf("%d:%s: [+] Paso 8 : Primer proceso hijo finalizado\n", 
            getpid(), __func__); 
 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_SUCCESS); 
    } 
 
    /* 9. En el proceso del daemon, conecte entrada/salida/error a /dev/null */ 
    printf("%d:%s: [+] Paso 9 : Conectar los fd 0,1,2 a dev null\n", 
        getpid(), __func__); 
 
    fd = open("/dev/null", O_RDWR); 
    if (fd < 0) { 
        printf("Fallo al abrir dev null\n"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE); 
    } 
 
    printf("--------- Pasos [10][11][12][13] o errores en syslog\n"); 
 
    if (dup2(fd, STDIN_FILENO) < 0 || 
         dup2(fd, STDOUT_FILENO) < 0 || 
         dup2(fd, STDERR_FILENO) < 0) {  
        syslog (LOG_ERR, "Fallo en dup2 STDIN/OUT/ERR_FILENO"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe 
        close(fd); 
        exit(EXIT_FAILURE);  
    } 
     
    close(fd); 
 
    /* 10. En el proceso del daemon, restablezca umask a 0 */ 
    syslog (LOG_NOTICE, "[+] Paso 10 : Reseteamos umask a 0"); 
    umask(0x0); 
 
    /* 11. Cambie el directorio actual al directorio raíz (/) */ 
    syslog (LOG_NOTICE, "[+] Paso 11 : Cambiamos el directorio a '/'"); 
    if (chdir("/") < 0) { 
        syslog (LOG_ERR, "Fallo en chdir a '/'"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE);  
    } 
 
    /* 12. Daemon: escriba el PID del daemon en un archivo PID */ 
    /* Abrimos o creamos el fichero para grabar el PID,  
        si no podemos bloquearlo finalizamos,  
        probablemente el demonio ya está en ejecución. */ 
    syslog (LOG_NOTICE, "[+] Paso 12 : Grabamos PID"); 
 
    pid_fd = open(pid_file_name, O_RDWR|O_CREAT, 0640); 
    if (pid_fd < 0) { 
        /* No podemos habrir el fichero */ 
        syslog (LOG_ERR, "No se puede crear o abrir el fichero %s\n", pid_file_name); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        exit(EXIT_FAILURE); 
    } 
    if (lockf(pid_fd, F_TLOCK, 0) < 0) { 
        /* No podemos bloquear el fichero */ 
        syslog (LOG_ERR, "No se puede bloquear el fichero %s\n", pid_file_name); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        close(pid_fd); // Cerramos el fichero para grabar el PID  
        exit(EXIT_FAILURE); 
    } 
 
    snprintf(str, sizeof(str), "%d\n", getpid()); 
    /* Escribimos el PID en el fichero */ 
    if (write(pid_fd, str, strlen(str)) == -1) { 
        syslog (LOG_ERR, "Error al escribir el pid en %s\n", pid_file_name); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        close(pid_fd); // Cerramos el fichero para grabar el PID  
        exit(EXIT_FAILURE);  
    } 
 
    /* 13. Daemon: elimine los privilegios innecesarios */ 
    syslog (LOG_NOTICE, "[+] Paso 13 : Reducimos privilegios"); 
 
    if (setgid (dgroupID) == -1) { 
        syslog (LOG_ERR, "Error en setgid"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        close(pid_fd); // Cerramos el fichero para grabar el PID  
        exit(EXIT_FAILURE);  
    } 
 
    if (setuid (duserID) == -1) { 
        syslog (LOG_ERR, "Error en setuid"); 
        close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
        close(pid_fd); // Cerramos el fichero para grabar el PID  
        exit(EXIT_FAILURE);  
    } 
 
    if (getresuid(&ruid, &euid, &suid) == -1) 
        syslog (LOG_ERR, "Error en getresuid"); 
    else 
        syslog (LOG_NOTICE, "UID: (%ld) (%ld) (%ld)\n", 
            (long) ruid, (long) euid, (long) suid); 
 
    if (getresgid(&rgid, &egid, &sgid) == -1) 
        syslog (LOG_ERR, "Error en getresgid"); 
    else 
        syslog (LOG_NOTICE, "GID: (%ld) (%ld) (%ld)\n", 
            (long) rgid, (long) egid, (long) sgid); 
 
 
    /* 14. Daemon: notificamos que la inicialización está completa */ 
    snprintf(mes_pipe, sizeof(mes_pipe),  
        "%d:%s: [+] Paso 14 : Nofiticamos al proceso original (por pipe)" 
        "\n\t\t\t (daemon creado correctamente)",  
        getpid(), __func__); 
 
    write(pipefd[1], mes_pipe, strlen(mes_pipe)); 
    close(pipefd[1]); // Cerramos el descriptor de escritura del pipe  
} 
 
int main() 
{ 
    uid_t ruid, euid, suid; 
    struct sigaction sa; 
    struct sockaddr_in sAddr; 
    fd_set readset, testset; 
    int listenSocket; 
    int newsock; 
    char lbuffer[100]; 
    char ebuffer[200]; 
    int result; 
    ssize_t nread; 
    int x; 
    int val; 
     
    if (getresuid(&ruid, &euid, &suid) == -1) { 
        printf ("Error en getresuid\n"); 
        exit(EXIT_FAILURE); 
    } else if (euid != 0) { /* solo comprobamos el usuario efectivo */ 
        printf ("Es necesario lanzar el daemon como root\n"); 
        exit(EXIT_FAILURE);  
    } 
 
    daemonize(); 
 
    /* Añadimos manejador para señales, finalizar y releer configuración */ 
    memset(&sa, 0, sizeof(sa)); 
    sigemptyset(&sa.sa_mask); 
    sa.sa_flags = SA_RESTART; 
    sa.sa_handler = handle_signal; 
    if (sigaction (SIGINT, &sa, NULL) == -1) 
        exit(EXIT_FAILURE);  
    if (sigaction (SIGHUP, &sa, NULL) == -1) 
        exit(EXIT_FAILURE); 
 
    syslog (LOG_NOTICE, "---> daemon_sysv iniciado."); 
 
    /* Creamos un socket 
    *  
    * Si queremos usar un puerto protegido < 1024 tendríamos que abrirlo  
    * antes de llamar a daemonize(), ya que en esta función cambiamos el  
    * usuario a uno sin privilegios root y cambiar el paso 1 donde cerramos 
    * todos los descriptores, cerrardo todos menos ese.  
    */ 
 
    listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
 
    val = 1; 
    result = setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); 
    if (result < 0) { 
        perror("server1"); 
        exit(EXIT_FAILURE); 
    } 
 
    sAddr.sin_family = AF_INET; 
    sAddr.sin_port = htons(1973); 
    sAddr.sin_addr.s_addr = INADDR_ANY; 
 
    result = bind(listenSocket, (struct sockaddr *) &sAddr, sizeof(sAddr)); 
    if (result < 0) { 
        perror("server1"); 
        exit(EXIT_FAILURE); 
    } 
 
 result = listen(listenSocket, 5); 
 if (result < 0) { 
 perror("server1"); 
 exit(EXIT_FAILURE); 
    } 
 
    FD_ZERO(&readset); 
    FD_SET(listenSocket, &readset); 
 
    SignalSalir = 0; 
 
    syslog (LOG_NOTICE, "---> daemon_sysv esperando peticiones."); 
 
    while (SignalSalir == 0) { 
        //TODO: Insertar el codigo del daemon aquí. 
        testset = readset; 
        result = select(FD_SETSIZE, &testset, NULL, NULL, NULL); 
 
        // Select interrumpido, si es por una señal errno==EINTR 
        if (result < 1 && errno!=EINTR) { 
 perror("server1"); 
 exit(EXIT_FAILURE); 
        }  
 
        if(SignalSalir == 1) 
            break; 
        if (SignalReload == 1) { 
            SignalReload = 0; 
            syslog (LOG_NOTICE, "Releemos configuración."); 
            continue; 
        }  
 
        for (x = 0; x < FD_SETSIZE; x++) { 
            if (FD_ISSET(x, &testset)) { 
                if (x == listenSocket) { 
                    newsock = accept(listenSocket, NULL ,NULL); 
                    FD_SET(newsock, &readset); 
                } else { 
                    nread = recv(x, lbuffer, 100, 0); 
                    if (nread <= 0) { 
                        close(x); 
                        FD_CLR(x, &readset); 
                        syslog (LOG_NOTICE, "Descriptor de cliente #%i desconectado\n", x); 
                    } else { 
                        lbuffer[nread] = '\0'; 
                        snprintf(ebuffer, sizeof(ebuffer),  
                            "daemon_sysv (%d): %s", getpid(), lbuffer); 
                        send(x, ebuffer, strlen(ebuffer), 0); 
                    }  
                } 
            } 
        }  
    }  
 
    close(listenSocket); 
 
    if (pid_fd != -1) { 
        if (lockf(pid_fd, F_ULOCK, 0) < 0) // No podemos desbloquear el fichero 
            syslog (LOG_ERR, "No se puede desbloquear el fichero %s\n",  
                pid_file_name); 
        else 
            syslog (LOG_NOTICE, "Fichero %s desbloqueado\n", pid_file_name); 
        close(pid_fd); // Cerramos el fichero donde se graba el PID  
    } 
 
    syslog (LOG_NOTICE, "<--- daemon_sysv finalizado."); 
    closelog(); 
 
    exit(EXIT_SUCCESS); 
} 

Probando el daemon

Vamos a ejecutar el daemon, primero abrimos una ventana para ver los mensajes syslog con journalctl:

journalctl -f

En otra ventana lanzamos el daemon como root:

sudo ./daemon_sysv
9579:daemonize: [+] Paso 1 : Cerramos fd's (3 a 1048576)
9579:daemonize: [+] Paso 2 : Reseteamos los manejadore se señales (1 a 64)
9579:daemonize: [+] Paso 3 : Reseteamos signal mask
9579:daemonize: [+] Paso 4 : Borramos las variables de entorno
9579:daemonize: [+] Paso 5 : Primer fork
9580:daemonize: [+] Paso 6 : Creamos una nueva sesión
9580:daemonize: [+] Paso 7 : Segundo fork
9580:daemonize: [+] Paso 8 : Primer proceso hijo finalizado
9581:daemonize: [+] Paso 9 : Conectar los fd 0,1,2 a dev null
--------- Pasos [10][11][12][13] o errores en syslog
9581:daemonize: [+] Paso 14 : Nofiticamos al proceso original (por pipe)
(daemon creado correctamente)
9579:daemonize: [+] paso 15 : Finalizando el proceso padre primario

El número al comienzo de cada línea es el id del proceso, (en cada caso lógicamente será distinto). Vemos como el proceso padre es el 9579 en este caso, el primer hijo es el 9580 que luego finalizamos y el segundo fork es el 9581 que será el pid de nuestro daemon. A partir del paso 9 cerramos los descriptores 0, 1, 2 para el daemon (9581), por lo que no podemos seguir mostrando información por pantalla y la enviamos al log del sistema. El paso 14 lo imprime el proceso padre (9579) que recibe la información por un pipe.

Lo normal es que un daemon se ejecute al arranque o vía crom, es decir, no es interactivo, por lo que no debíamos sacar nada por pantalla. Para los log, o bien generamos un fichero independiente de logs o logamos en el log de sistema (o ambos).

En la ventana donde se muestra el journal nos saldrá algo similar a esto:

jul 12 13:19:47 sglan-pc7 sudo[9578]:    david : TTY=pts/0 ; PWD=/home/david/Proyects/cfunc/daemon_sysv ; USER=root ; COMMAND=./daemon_sysv
jul 12 13:19:47 sglan-pc7 sudo[9578]: pam_unix(sudo:session): session opened for user root by (uid=0)
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: [+] Paso 10 : Reseteamos umask a 0
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: [+] Paso 11 : Cambiamos el directorio a '/'
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: [+] Paso 12 : Grabamos PID
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: [+] Paso 13 : Reducimos privilegios
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: UID: (1000) (1000) (1000)
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: GID: (1000) (1000) (1000)
jul 12 13:19:47 sglan-pc7 daemon_sysv[9581]: ---> daemon_sysv iniciado.
jul 12 13:19:47 sglan-pc7 sudo[9578]: pam_unix(sudo:session): session closed for user root

En el log del sistema mostramos los pasos 10-13 y también mostramos el UID y GID finales, que son los privilegios con los que correrá el daemon (recordemos que arrancamos como root).

Para comprobar que el daemon esta corriendo correctamente podemos usar ps:

ps -C daemon_sysv -o "pid ppid pgid sid tty command"
PID PPID PGID SID TT COMMAND
9581 1 9580 9580 ? ./daemon_sysv

Comprovamos que el proceso padre es init (PPID = 1), el damon no controla ningún terminal (TT = ?) y el proceso no es el líder de la sesión (PID != SID).

Desde otro terminal vamos a usar la herramienta netcat para probar el daemon:

nc localhost 1973
Comunidad Libre
daemon_sysv (9581): Comunidad Libre
^C

Como utiliamos multiplexing en el daemon podemos hacer peticiones concurrentes desde varios clientes. Podemos mandar dos señales al daemon, SIGHUP para releer la configuración (en este caso no hace nada excepto mostrar un mensaje en el log del sistema):

kill -SIGHUP 9581  

en el log veremos algo como esto:

jul 12 13:58:23 sglan-pc7 daemon_sysv[9581]: Releemos configuración

o finalizar el daemon con:

kill -SIGINT 9581

en el log tendremos:

jul 12 13:59:12 sglan-pc7 daemon_sysv[9581]: <--- daemon_sysv finalizado.

Bueno, esta es más o menos la forma de crear daemons clásicos sysV, que no podemos decir que sea muy intuitiva. En la próxima entrada veremos la forma de crear daemons para systemd.

 

Modificado por última vez enViernes, 14 Agosto 2020 20:02
(0 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.