Herramientas de usuario

Herramientas del sitio


highlevel:c:clinux

¡Esta es una revisión vieja del documento!


C++ en Linux

General

  • Los directorios por defecto de los includes en linux son /usr/include y /usr/local/include.

El entorno

Funciones generales

  • char* getenv (char* var) consulta la variable de entorno indicada en var.

Directorios

Saber directorio actual

  • Podemos consultar la variable de entorno $PWD.
  • La función char *getcwd(char *buf, size_t size) coloca la ruta del directorio actual en la variable buf, size corresponde al tamaño de dicha variable.
  • La función char *get_current_dir_name(void)
  • La función char *getwd(char *buf)
char *directorioPtr, *pathPtr;
pathPtr = getenv( "PATH" );
printf("%s\n", pathPtr);

Cambiar directorio actual

  • int chdir(const char *path), recibe el directorio donde cambiar mediante la ruta de este.
  • int fchdir(int fd), recibe el directorio donde cambiar como descriptor de fichero.

Estas dos funciones devuelven 0 si el cambio ha sido correcto y -1 si no.

Creación y eliminación

  • int mkdir(const char *pathname, mode_t mode) crea un directorio en la ruta pathname en un modo mode, este es el número que representa sus permisos (generalmente 755).
  • int rmdir(const char *pathname) elimina el directorio indicado en pathname.

Listado

Las funciones que necesitamos llamar para listar el contenido de un directorio son:

  • DIR *opendir(const char *name)
  • dirent *readdir(DIR *dir)
  • int closedir(DIR *dir)

Abriremos y cerraremos un directorio con las funciones opendir y closedir respectivamente, la primera devuelve una variable DIR que utilizaremos para llamar a readdir que a su vez de vuelve una variable de la estructura dirent que tiene el siguiente formato:

struct dirent {
   ino_t d_ino;                     // numero de i-node de la entrada de directorio
   off_t d_off;                     // offset
   wchar_t d_reclen;                // longitud de este registro
   char d_name[MAX_LONG_NAME+1]     // nombre de esta entrada
}

La función readdir cada vez que se la llame devolverá un nombre de archivo distinto, recorrerá así el directorio; habrá pasado por todos cuando devuelva NULL.

Descriptores

Debido a que en Unix\Linux todo tiene una representación de fichero las funciones a más bajo nivel tienen que ver con estos. Los descriptores son la dirección en memoria necesaria para acceder a los recursos del sistema (ficheros, pantalla, sockets…) que tienen forma de ficheros.
Las funciones básicas para la apertura, creación, cerrado, escritura y lectura de descriptores son: open, creat, close, read y write.

Procesos

Los procesos dentro del SO tienen un identificador denominado pid, este puede ser cogido mediante la función getpid(); también podremos coger el pid del padre mediante la función getppid().
Podremos crear procesos mediante las funciones system, exec o fork.

system

Utiliza el shell para lanzar un comando. El proceso donde es llamado quedará en espera del retorno de la función que será el integer correspondiente al retorno del proceso\comando lanzado.

int main(int argc, char** argv) {
  system("ls -la");
  return 0;
}

fork

Duplica el proceso padre, copiando sus variables con sus respectivos valores en memoria, el proceso creado a partir de este será llamado proceso hijo y este proceso padre. El proceso hijo se ejecutará a partir de donde ha sido creado y tendrá un pid distinto al del padre. Aún así, la forma para diferenciarlos será el retorno de la función fork que en el proceso padre retornará el pid del proceso hijo, en cambio en el proceso hijo retornará un 0.

int main(int argc, char** argv) {
  if (fork() == 0) 
    printf("Este es el proceso hijo\n");
  else
    printf("Este es el proceso padre\n");
  return 0;
}

exec

Reemplaza el proceso actual por otro proceso, es decir, detiene la ejecución del proceso donde es llamado y lanza el comando indicado, dando a la ejecución de este el identificador del proceso actual.
Las variantes del exec son:

  • Variantes que contienen p (execvp o execlp) buscan en el path actual el comando pasado (si el nombre de función no llevase p habría que indicar el path completo).
  • Variantes que contienen v (execvp, execv o execve) aceptan la lista de parámetros del comando como un array de strings.
  • Variantes que contienen l (execlp, execl o execle) aceptan la lista de parámetros del comando en formato de argumentos de C.

Una forma de trabajar con exec es, primero hacer un fork del proceso y luego lanzar un comando mediante exec

int child_pid;
char* arg_list[] = {
  "ls",     // El primer argumento ha de ser el nombre del programa
  "-l",
  "/",
  NULL      // El último elemento de la lista ha de ser NULL
};
 
child_pid = fork ();
 
if (child_pid != 0)
  return child_pid;
else {
  execvp ("ls", arg_list);
  fprintf (stderr, "Error en llamada de execvp\n");
  abort ();
}
 
printf ("Done!\n");
return 0;

Otras funciones

Terminar un proceso

Para usar la función kill deberemos incluir signal.h y sys/types.h, dicha función acaba un proceso hijo indicado. Recibe por parámetro el pid del proceso y una señal de finalización, esta puede ser:

  • SIGINT, es como si el sistema enviase un ctrl+c.
  • SIGTERM, el enviado por el comando del sistema kill.
  • SIGKILL, la más poderosa ya que acaba el proceso sin contemplaciones.
  • SIGABRT, SIGBUS, SIGSEGV, y SIGFPE.

Esperar un proceso

  • La función wait bloquea el proceso donde es llamada esperando a que un proceso hijo cualquiera acabe. Se le ha de pasar una referencia a un integer donde colocará el retorno del proceso hijo.
  • La función waitpid funciona igual que wait sólo que a esta se le pasa el pid del proceso hijo que esperará.

Comunicación entre procesos

Pipes

Una pipe es una conexión de la salida de un proceso a la entrada de otro. Esto es lo que ocurre cuando en la terminal hacemos:

command1 | command2

Internamente, lo que está pasando es:

  • La entrada standard de command1 viene del teclado.
  • La salida standard de command1 está conectada a la entrada de command2.
  • La salida standard de command2 es la salida standard.

popen y pclose

FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);

La forma más rápida de recibir los datos de un comando es mediante una pipe, y la forma más rápida de crear una pipe es utilizando las funciones popen y pclose. La primera (popen) devuelve un puntero a una variable FILE, recibe el comando que se lanzará y la forma en la que es la pipe (de lectura r o de escritura w), por lo que se podrá leer la salida con fread o escribir con fwrite. La función pclose cerrará una pipe abierta por popen.

FILE *read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
 
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("uname -a", "r");
 
if (read_fp != NULL) {
  chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
  if (chars_read > 0)
    printf("Output was:-\n%s\n", buffer);
  pclose(read_fp);
  exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);

Función pipe

La función pipe crea una pipe a partir de un array de integers de tamaño 2, en este guardará dos descriptores de ficheros, uno de entrada (el primer entero del array, que es abierto como solo lectura) y otro de salida (el segundo entero del array, que es abierto como solo escritura). Una vez creado el pipe se podrán hacer lecturas y escrituras como si de un fichero se tratase.
Las pipes son perfectas para la comunicación entre procesos padre-hijo, se crea antes de llamar a fork y, como estos descriptores se comparten entre padre e hijo, una vez sea llamado uno podrá utilizar un descriptor para enviar y el otro para recibir. Es aconsejable que si los dos quisieran enviar y recibir se creasen dos pipes, y que se cerrasen en los procesos adecuados los descriptores que no se utilizarán.
Se utilizarán las funciones write en la pipe[1] y read en la pipe[2].

int tuberia [2];
int idson;
char buffer[BSIZE];
int bytesReaded = 0;
 
pipe(tuberia);
idson = fork();
if (idson == 0) {
  // El hijo lee de la tuberia
  close (tuberia[1]);
  while ((bytesReaded = read(tuberia[0], buffer, BSIZE)) > 0) 
    write(1, buffer, bytesReaded);
 
  close(tuberia[0]);
} else {
  // El padre escribe por la tuberia
  close (tuberia[0]);
  strcpy(buffer, "hola caracola\n");
  write(tuberia[1], buffer, strlen(buffer));
  close(tuberia[1]);
  waitpid(idson, NULL, 0);
}

Señales

Threads

Los threads se ejecutan dentro de un proceso que, a diferencia de estos, al crearse un thread no se copia su espacio de memoria sino que este es compartido con los demás threads y con el proceso principal.
Es el SO el que decide cuando un thread se ejecutará, es por ello que para la programación de estos no es correcto hacerlo pensando en que seguirán un orden concreto.
El tipo identificador de thread es denominado pthread este nombre viene de POSIX thread.

Para utilizar los pthreads debemos linkar la librería pthread (desde línea de comandos utilizando -lpthread) e incluir pthread.h.

Recopilación

Tipos de datos

  • pthread_t: manejador de thread.
  • pthread_attr_t: atributos de un thread.

Funciones para el manejo de threads

  • pthread_create(): crea un thread
  • pthread_exit(): acaba el thread actual
  • pthread_cancel(): cancela la ejecución de un thread
  • pthread_join(): bloquea el thread actual hasta que otro acabe
  • pthread_attr_init(): inicializa los atributos de un thread
  • pthread_attr_setdetachstate()
  • pthread_attr_getdetachstate()
  • pthread_attr_destroy(): destruye los atributos de un thread
  • pthread_kill(): envia una señal de finalización a un thread

Funciones de sincronización

  • pthread_mutex_init(): Inicializa un mutex
  • pthread_mutex_destroy()
  • pthread_mutex_lock(): bloquea el mutex
  • pthread_mutex_trylock(): bloquea el mutex sin bloquear el proceso
  • pthread_mutex_unlock(): desbloquea el mutex
  • pthread_cond_init()
  • pthread_cond_destroy()
  • pthread_cond_signal()
  • pthread_cond_wait()

Manejo de threads

Para crear un thread utilizamos la función pthread_create pasándole los siguientes argumentos:

  • thread, una referencia al identificador del thread.
  • attr, una referencia al objeto que contiene los atributos, si queremos los valores por defecto asignaremos NULL.
  • start_routine, la función en la que se ejecutará el thread, del estilo void *nombre (void *param).
  • arg, el argumento (en forma de puntero) que se le pasa a la rutina del thread.

Un thread puede finalizar de cualquiera de las siguientes formas:

  • La función acaba haciendo un return.
  • Se hace una llamada a pthread_exit dentro del thread.
  • El thread es cancelado por otro thread mediante pthread_cancel.
  • El proceso principal acaba.

Si el proceso principal acaba con un pthread_exit los threads vinculados a este no finalizarán y el proceso acabará cuando todos los threads acaben.

#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS     5
 
void *PrintHello(void *threadid) {
   int tid;
   tid = (int)threadid;
   printf("Hello World! It's me, thread #%d!\n", tid);
   pthread_exit(NULL);
}
 
int main (int argc, char *argv[]) {
   pthread_t threads[NUM_THREADS];
   int rc, t;
   for(t=0; t<NUM_THREADS; t++){
      printf("In main: creating thread %d\n", t);
      rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
      if (rc){
         printf("ERROR; return code from pthread_create() is %d\n", rc);
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

La función pthread_join bloquea el thread actual hasta que el thread indicado no acabe (se puede impedir que un thread sea joinable, es decir que no pueda esperarse, si se crea como detached, para ello hay que hacerlo con los atributos de este).
La función pthread_self devuelve el identificador del thread actual.

Sincronización

Consiste en la realización de operaciones atómicas, esto es que no pueden ser interrumpidas por el cambio de thread en ejecución realizado por el procesador, para que los threads no accedan a recursos compartidos entre ellos a la vez lo que provocaría acciones erróneas.
Entre otras formas de sincronización existen: los mutexes, los semáforos, las variables condicionales…

Mutexes

Un mutex es como una puerta a una porción de memoria compartida por los threads con la cual hacen operaciones. Cuando un thread pasa por dicha puerta la cierra (si estubiese cerrada no podría pasar), operación de mutex lock, cuando acaba las operaciones abre la puerta, unlock. La metodología para utilizar los mutex es la siguiente:

  1. Crear e inicializar una variable mutex.
  2. Varios threads intentarán entrar al mutex.
  3. Sólo uno conseguirá adueñarse de este.
  4. El propietario realiza las acciones que desee.
  5. El propietario desbloquea el mutex.
  6. Otro thread accede al mutex y repite el proceso.
  7. Al final el mutex se destruye.

Un bloqueo del mutex que no bloquea el thread es una llamada a la función trylock.
Cuando se inicializa un mutex este lo hace desbloqueado.
Las funciones que se utilizan son:

  • pthtread_mutex_lock que se usa para adquirir y bloquear un mutex, si el mutex ya está bloqueado la llamada a esta función bloqueará el thread hasta que el mutex sea desbloqueado.
  • pthread_mutex_unlock que desbloqueará el mutex. Es una función necesaria de llamar después de haber realizado los cálculos, Retornará error si el mutex ya estaba desbloqueado o si pertenecía a otro thread.

Podemos inicializar un mutex con los atributos por defecto utilizando la macro PTHREAD_MUTEX_INITIALIZER:

pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;

Sockets

X Window

Las interficies gráficas (GUI) en Linux, en su nivel más bajo, son manejadas por las X Window (o X11 o símplemente las X). Consiste en una arquitectura cliente-servidor, en la que el servidor realiza el enlace con el hardware (ratón, pantalla, teclado…) y el cliente recibe datos de este y muestra las aplicaciones, por lo que podemos decir que los sistemas de escritorios son los clientes de las X. Una de las ventajas de este modo de comunicación es que el cliente no tiene por qué estar en la misma máquina que el servidor, ni tampoco tiene por qué ser una pantalla ya que puede ser cualquier dispositivo gráfico.
El X Server gestiona la pantalla, la entrada por teclado y por ratón. Cuando el usuario hace click sobre una posición concreta de la pantalla el X Server envia los datos a la aplicación adecuada. Los mensajes que recibirá el X Server serán de redibujar ventanas.
La comunicación entre cliente-servidor se realiza mediante el XProtocol.
Xlib es la librería que se ha de agregar para trabajar a bajo nivel con las X, es tan de bajo nivel que únicamente se utiliza para dibujar sobre la ventana (por ejemplo un menú requeriría de un gran número de líneas de código). Es por eso que para desarrollar una aplicación generalmente se utiliza un Toolkit (GTK, Qt…).

Para desarrollar con XLib deberemos linkar la librería X11 e incluir X11/Xlib.h y X11/Intrinsic.h (para poder disponer de este último necesitarás añadir el paquete libxt-dev).

Manejo de ventanas

Las funciones de XLib necesitan de una variable Display, esta equivale al enlace entre el cliente y el servidor, para conseguir una debemos llamar a la funcion XOpenDisplay la cual recibe el nombre de la pantalla. El nombre de la pantalla por defecto podemos encontrarlo en la variable de entono DISPLAY (aunque también podríamos pasarle NULL):

Display *dpy = XOpenDisplay(getenv("DISPLAY"));

Para la creación de ventanas utilizaremos la funciones XCreateWindow (función general para crear nuevas ventanas) y la XCreateSimpleWindow (función que crea una subventana utilizando los valores por defecto de la aplicación padre). Estas funciones requieren de una variable Window (la ventana padre) y una variable Visual (almacena la estructura de color de las X). Para conseguirlas podemos utilizar las siguientes funciones:

Visual *vis = DefaultVisual(dpy,0);            // Devuelve el Visual por defecto de la Dislay
Window w = DefaultRootWindow(dpy);             // Devuelve la ventana raíz de la Display

Las funciones XCreateWindow y XCreateSimpleWindow devuelven una variable Window, esta no es más que un integer identificador de la ventana. Sus definiciones son:

Window XCreateWindow(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes)
      Display *display;                  // Display utilizado
      Window parent;                     // Ventana padre
      int x, y;                          // Posición de la ventana
      unsigned int width, height;        // Tamaño
      unsigned int border_width;         // Tamaño del borde
      int depth;                         // Profundidad de color
      unsigned int class;                // Clase Visual
      Visual *visual                     // La variable Visual
      unsigned long valuemask;           // 
      XSetWindowAttributes *attributes;  // Atributos de la ventana

Window XCreateSimpleWindow(display, parent, x, y, width, height, border_width, border, background)
      Display *display;
      Window parent;
      int x, y;
      unsigned int width, height;
      unsigned int border_width;
      unsigned long border;
      unsigned long background;

Tanto la clase Visual como la profundidad de color podemos coger las de por defecto mediante el comando CopyFromParent:

Window w;
vis = DefaultVisual(dpy,0);
w = XCreateWindow(dpy, 
  DefaultRootWindow(dpy),
  100, 100,              
  320, 200,
  0,                         
  CopyFromParent,            
  CopyFromParent,            
  vis,                       
  0, NULL);                  

Una vez creada una ventana necesitaremos hacerla visible, para ello debemos mapearla mediante la función XMapWindow.
Podremos asignar el nombre (título) de la ventana con XStoreName.
Mientras llamemos a la función XCheckWindowEvent la ventana se mostrará.

main() {
  Display* dpy = XOpenDisplay(NULL);
  Visual *vis;
  Window w;
  vis = DefaultVisual(dpy,0);
 
w = XCreateWindow(dpy, 
    DefaultRootWindow(dpy),
    100, 100,              
    320, 200,
    0,                         
    CopyFromParent,            
    CopyFromParent,            
    vis,                       
    0, NULL);       
 
  XStoreName(dpy, w, "hello");
  XMapWindow(dpy, w);
 
  XCheckWindowEvent(dpy,w,0,0);
 
  while(1) {}
}

La función para desmapear una ventana es XUnmapWindow y para destruir la ventana XDestroyWindow. Si quisiesemos destruir todas las sub-ventanas de una aplicación utilizaríamos XDestroySubWindows.

Gestión de eventos

Para interceptar eventos de en las X debemos indicar de qué eventos queremos se nos avise, para ello XSelectInput que recibe la Display, la Window y los siguientes eventos de la lista:

0, KeyPressMask, KeyReleaseMask, ButtonPressMask, ButtonReleaseMask, EnterWindowMask, 
LeaveWindowMask, PointerMotionMask, PointerMotionHintMask, Button1MotionMask, Button2MotionMask, 
Button3MotionMask, ButtonMotionMask, ExposureMask, VisibilityChangeMask, ResizeRedirectMask, FocusChangeMask

Para tratar los eventos tenemos las siguientes funciones:

  • XCheckWindowEvent
  • XNextEvent
  • XPeekEvent
  • XWindowEvent
  • XCheckWindowEvent
  • XMaskEvent
  • XCheckMaskEvent
  • XCheckTypedEvent
  • XCheckTypedWindowEvent

Dibujando en una ventana

Librerías

Estáticas

Dinámicas

Otros

Insertar código ensamblador

Ensamblador inline

gcc permite insertar código ensamblador en medio de código C, este código es denominado inline assembly y, al ser en Linux, sigue la sitnaxis AT&T.
Para la inserción de código inline utilizaremos la función especial es __asm__, su sintaxis es:

__asm__  ("instrucciones" : lista_salida : lista_entrada : lista_destruida);

Las instrucciones es un string donde está el código ensamblador. La lista de salida son los registros donde se guardará algún dato. La lista de entrada son los registros y variables que se utilizaran dentro del código. La lista destruida son los registros que han sido modificados por las instrucciones (para que gcc sepa cuales ha de reservar en un futuro). A las variables y registros utilizados en el código se les hace referencia a partir de un índice (que empieza por 0 (indicado con %) y cuenta a partir de la lista de salida y se les une las demás).

int i = 100;
__asm__ ("movl %0, %%eax" : : "g" (i));

En este caso %0 corresponde al valor de entrada i; el acceso al registro eax se hace mediante %% (esto se ha de hacer para cada registro); lo que el código hace es asignar el valor de i (100) a eax.
La directiva “g”(i) indica al compilador donde almacenar el parámetro. “r” le permitiría guardar la variable en cualquier registro qeu no esté en uso. “a” en ax\eax, “b” en bx\ebx, “c” en cx\ecx, “d” en dx\edx, “D” en di\edi, “S” en si/esi, etc.

Herramientas

gcc y g++

Son los compiladores de C y C++ (gcc y g++ respectivamente) en linux.

  • Para compilar un archivo .c haríamos: gcc hola.c.
  • Para compilar un archivo .c e indicar el nombre del resultado: gcc -o hola hola.c
  • El parámetro -c hace que no salga un ejecutable sino uno con código objeto.
  • Para realizar el linkage de librerías estáticas (ficheros .lib) en tu proyecto tendrás que utilizar el parámetro -l seguido del nombre de la librería, pero seguido sin espacios; por ejemplo, tenemos la librería ddraw.lib o gdi32.lib haremos -lddraw o -lgdi32.
  • el parámetro -s saca el archivo en ensamblador del compilado.

Debug

Si compilamos un código con el parámetro -g podremos ejecutar el comand gdb <archivo salida> y nos aparecerá una consola de debugación, los comandos que pueden sernos útiles son:

  • run para iniciar el programa.
  • b file:line, o break file:line, para poner un breakpoint en una línea del programa.
  • delete, elimina todos los breakpoints.
  • kill para detener la ejecución del programa.
  • c o continue para continuar la ejecución de un programa parado.
  • quit para salir de gdb.
  • n ejecuta la siguiente instrucción.
  • s ejecuta la siguiente instrucción entrando en funciones.
  • print variable muestra el valor de la variable indicada.
  • list muestra las líneas que rodean la actual.
  • info r muestra los valores de los registros.
  • info stack muestra la pila.
  • info line muestra información sobre la línea actual.
  • info locals muestra las variables locales.
  • source <fichero> ejecuta un script de gdb.
  • add-symbol-file <fichero> agrega otro fichero al debug.

Notas

Documentos

highlevel/c/clinux.1287932150.txt.gz · Última modificación: 2020/05/09 09:24 (editor externo)