Proyecto pipex del cursus 42.
Estre proyecto va sobre el manejo de pipes.
Pipe: herramienta que permite redireccionar la salida estándar (stdout) de un proceso a la entrada estándar (stdin) de otro proceso. Permite la comunicación, el intercambio de datos, entre procesos relacionados.
El proyecto consiste en crear un programa que se ejecute de la siguiente manera: ./pipex infile cmd1 cmd2 outfile y emule el comportamiento del comando < infile cmd1 | cmd2 > outfile, donde:
- infile y outfile son nombres de archivos.
- cmd1 y cmd2 son comandos de shell.
¿Qué hace este comando?
-
Toma el contenido de infile y lo envía como entrada al cmd1. Equivalente a cmd1 < infile.
-
El pipe (|) conecta la salida del cmd1 con la entrada del cmd2. El resultado de ejecutar el cmd1 se pasa directamente al cmd2 sin guardarlo en un archivo temporal. Así, se ejecutan dos comandos en cadena.
-
La salida de cmd2 se guarda en outfile. Si el archivo existe, se sobreescribe; si no, se crea uno nuevo.
| open() | |
|---|---|
| ¿Qué hace? | Abre un archivo y obtiene su file descriptor (fd) |
| Prototipo | int open(const char *pathname, int flags, mode_t mode) |
| Argumentos | pahtname: ruta del archivo. flags: entero que especifica el modo en el que se abre el archivo. Algunos modos comunes: - O_RDONLY: solo lectura. - O_WRONLY: solo escritura. - O_RDWR: lectura y escritura. - O_CREAT: crea el archivo si no existe. - O_TRUNC: si el archivo existe, elimina su contenido. - O_APPEND: escribe al final del archivo sin truncarlo. mode: solo se utiliza con O_CREAT, establece los permisos del archivo, expresados en octal. |
| Return | Devuelve un fd en caso de éxito y -1 en caso de error. |
| close() | |
|---|---|
| ¿Qué hace? | Cierra un archivo que ha sido abierto mediante open(). Al cerrarlo, el sistema libera el fd y cualquier recurso asociado, permitiendo que el SO reutilice el descriptor para otras operaciones. |
| Prototipo | Prototipo: int close(int fd) |
| Return | Devuelve 0 en caso de éxito y en caso de error, -1 y establece errno . |
| read() | |
|---|---|
| ¿Qué hace? | Lee datos desde un archivo (a partir de su fd) o entrada (como stdin). |
| Prototipo | ssize_t read (int fd, void *buffer, size_t count) |
| Argumentos | fd: file descriptor desde el que se quiere leer. Se obtiene mediante open() o dup() o un descritor estándar como 0 (stdin). buffer: puntero a un espacio de memoria donde se almacenarán los datos leídos. count: número máximo de bytes que se intentarán leer. |
| Return | Devuelve el número de bytes leídos. Si se alcanza el final del archivo (EOF), devuelve 0. Devuelve -1 y establece errno en caso de error. |
| perror() | |
|---|---|
| ¿Qué hace? | Imprime un mensaje de error en stderr. Muestra un mensaje personalizado junto con la descripción del error correspondiente a errno. |
| Prototipo | void perror(const char *s) |
| Argumentos | s: Mensaje personalizado a imprimir antes de la descripción del error. |
| strerror() | |
|---|---|
| ¿Qué hace? | Convierte el valor de errno en un mensaje de error. |
| Prototipo | char *strerror(int errnum) |
| Argumentos | errnum: código de error a convertir en un mensaje de error. Típicamente es el valor de errno después de una operación fallida. |
| Return | Devuelve un puntero a una cadena con el mensaje de error correspondiente al valor de errnum. |
| access() | |
|---|---|
| ¿Qué hace? | Comprueba la accesibilidad de un archivo o directorio. |
| Prototipo | int access(const char *pathname, int mode) |
| Argumentos | pahtname: cadena que representa la ruta. mode: entero que especifica el tipo de acceso que queremos verificar. - F_OK: verifica la existencia del archivo. - F_OK: verifica la existencia del archivo. - R_OK: accesible para lectura. - W_OK: accesible para escritura. - X_OK: accesible para ejecución. |
| Return | Devuelve 0 si el acceso especificado está permitido, en caso contrario devuelve -1 y esatblece errno. |
| dup() | |
|---|---|
| ¿Qué hace? | Duplica fds. Útil para redirigir entradas y salidas, especialmente en procesos que necesitan manipular la stdin, la stdout o la stderr. |
| Prototipo | int dup(int oldfd) |
| Argumentos | oldfd: file descriptor que se desea duplicar. |
| Return | Devuelve el nuevo fd duplicado o 1 en caso de error, estableciendo errno. El nuevo fd apunta al mismo archivo que oldfd, comparten el mismo puntero de archivo y hereda los mismos permisos de acceso. |
| dup2() | |
|---|---|
| ¿Qué hace? | Duplica un fd en un nuevo fd. |
| Prototipo | int dup(int oldfd, int newfd) |
| Argumentos | oldfd: fd que se desea duplicar. newfd: nuevo fd. Si newfd está en uso, dup2 lo cierra antes de duplicar oldfd en él. |
| Return | Devuelve newfd o -1 en caso de error. |
| execve() | |
|---|---|
| ¿Qué hace? | Reemplaza el proceso actual con un nuevo programa. Se utiliza para ejecutar un programa especificado en un archivo ejecutable, con el entorno y los argumentos que se le proporcionan. |
| Prototipo | int execve(const char *filename, char *const argv[], char *const envp[]) |
| Argumentos | filename: ruta del archivo ejecutable. argv: array de cadenas que representan los argumentos del programa. envp: array de cadenas que contiene las variables de entorno. |
| Return | En caso de éxito, no devuelve nada; el proceso actual se reemplaza por el nuevo programa y la ejecución continúa desde el punto de entrada del nuevo programa. En caso de error devuelve -1 y establece errno. |
| exit() | |
|---|---|
| ¿Qué hace? | Termina un programa de manera controlada. Permite salir de un programa con un código de estado que indica si la salida fue exitosa o si ocurrió un error. |
| Prototipo | void exit(int status) |
| Argumentos | status: entero que se devuelve al SO cuando el programa termina. Este valor puede indicar éxito (0) o error (valor distinto de 0). |
| fork() | |
|---|---|
| ¿Qué hace? | Genera un nuevo proceso hijo a partir de un proceso padre. El proceso hijo es una copia del proceso padre, pero tiene su propio PID (indentificador de proceso). El proceso padre y el proceso hijo se ejecutan simultáneamente, aunque pueden comportarse de manera diferente. |
| Prototipo | fd_t fork(void) |
| Return | En el proceso padre, fork() devuelve el PID del proceso hijo. El el proceso hijo, fork() devuelve 0. Si fork() falla, devuelve -1 en el proceso padre y no crea ningún proceso hijo. |
Ejemplo:
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("Fork: ");
exit(EXIT_FAILURE);
}
else if (pid == 0)
//Estamos en el proceso hijo
else //otro número positivo
//Estamos en el proceso padre
| pipe() | |
|---|---|
| ¿Qué hace? | Crea un canal de comunicación unidireccional entre procesos. Permite que un proceso (el escritor) envíe datos a otro proceso (el lector) a través de un buffer de memoria. pipe() crea un array de dos fds que representan los extremos del pipe. - pipefd[0]: extremo de lectura. - pipefd[1]: extremo de escritura. pipefd[1] escribirá en su fd y pipefd[0] leerá el pipefd[1] y escribirá en su propio fd. |
| Prototipo | int pipe(int pipefd[2]) |
| Return | Devuelve 0 en caso de éxito y en caso de error devuelve -1 y establece errno. |
| unlink() | |
|---|---|
| ¿Qué hace? | Elimina un nombre de archivos del sistema de archivos, lo que significa que el archivo ya no es accesible mediante ese nombre. El archivo no se elimina físicamente hasta que no haya más referencias a él. Contabilidad de enlaces: cada arhcivo en Unix/Linux tiene un contador de enlaces. Si el contador llega a 0 significa que no hay más nombres de archivo apuntando a los datos del archivo y el contenido se libera. Si aún hay otros nombres de archivo apuntando al mismo contenido, el archivo permanece en el disco. |
| Prototipo | int unlink(const char *pathname) |
| Argumentos | pathname: ruta del archivo a eliminar. |
| Return | Devuelve 0 en caso de éxito y -1 y errno en caso de error. Para eliminar un archivo, el proceso debe tener los permisos adecuados. Si no se tienen o el archivo está en uso, unlink() falla. Si el archivo está abierto al llamar a unlink(), su contenido no se elimina hasta que todos los fd de archivo que apuntan a él sean cerrados. |
| wait() | |
|---|---|
| ¿Qué hace? | Permite que un proceso padre suspenda su ejecución hasta que uno de sus procesos hijo termine. Cuando un proceso hijo finaliza, wait() recupera su estado de salida, lo que permite al proceso padre realizar tareas específicas dependiendo del resultado de la ejecución del hijo. |
| Prototipo | pid_t wait(int *status) |
| Argumentos | status: puntero a un entero donde wait() almacena el estado de salida del hijo. Este valor sirve para obtener información sobre cómo terminó el proceso hijo, normalmente mediante macros como WIFEXITED o WEXITSTATUS... |
| Return | Devuelve el PID del hijo que terminó en caso de éxito. Si no hay hijos a los que esperar, devuelve -1 y establece errno. |
| waitpid() | |
|---|---|
| ¿Qué hace? | Permite que un proceso padre espere a que un proceso hijo termine su ejecución. Es más flexible que wait porque permite especificar a qué proceso hijo se debe esperar. Útil cuando hay múltiples procesos hijo. |
| Prototipo | pid_t waitpid(pid_t pid, int *status, int options) |
| Argumentos | pid: puede ser el PID del proceso hijo al que se quiere esperar. Si es -1 indica que se espera a cualquier proceso hijo (comportamiento similar a wait()). Si es 0 espera a cualquier proceso hijo del mismo grupo de procesos. Si es otro valor negativo, espera a cualquier hijo cuyo grupo de procesos sea el mismo que el del proceso padre. status: puntero a un entero donde wait() almacena el estado de salida del hijo. Este valor sirve para obtener información sobre cómo terminó el proceso hijo, normalmente mediante macros como WIFEXITED o WEXITSTATUS... options: 0 indica el comportamiento estándar, espera a que el hijo termine. WNOHANG no bloquea, si no hay hijos que no hayan terminado, retorna inmediatamente. WUNTRACED también retorna si un hijo está detenido. |
| Return | Devuelve el PID del hijo que terminó en caso de éxito. En caso de error, devuelve -1 y establece errno. |
Usar waitpid() es esencial para evitar que los procesos hijos se conviertan en zombies. Cada vez que un hijo termina, si el padre no lo recoge, este proceso hijo permanece en la tabla de procesos como un proceso zombie hasta que el padre llame a una de estas funciones. Un proceso zombie es un proceso que ha temrinado su ejecución pero todavía tiene una entrada en la tabla de procesos del sistema.
Cómo comprobar procesos zomies: despues de ejecutar el programa, usar comando ps aux | grep pipex y ver si hay alguna 'Z' en la columna de estado (la octava columna). La columna de estado mostrará una Z indicando que el proceso está en estado zombie.
Gestionar múltiples pipes.
El programa debe ejecutarse así: ./pipex infile cmd1 cmd2 cmd3 ... cmdn outfile
Y tiene que emular el comportamiento del comando: < infile cmd1 | cmd2 | cmd3 ... | cmdn > outfile
¿Qué hace este comando?
Comandos adicionales en la misma cadena de tuberías. Cada comando tomará la salida del comando anterior como su entrada. La salida final del último comando se guarda en outfile.
Cada proceso intermedio debe tomar la entrada desde el pipe de lectura del proceso anterior(prev_pipefd[0]) y enviar su salida al pipe de escritura del proceso actual (pipefd[1]).
También tiene que aceptar << y >> cuando el primer parámetro es "here_doc":
./pipex here_doc LIMITADOR cmd cmd1 file
Debe comportarse como: cmd << LIMITADOR | cmd1 >> file
cmd << LIMITADOR: este comando se conoce como here-document o heredoc. Permite introducir múltiples líneas de texto como entrada para el comando. El texto se introduce hasta que se encuentra una línea que contiene solo "LIMITADOR".
|: toma la salida del comando anterior y la envía como entrada al siguiente comando.
cmd1 >> file: el comando recibe la entrada del pipe, >> es el operador de redirección para añadir (append) la salida al final del archivo. Si el archivo no existe, se crea.
graph LR;
A["***main.c***"] --> B["1 - Checkeo de argumentos."];
B --> BA["Si argc != 5"];
BA --> C["Mensaje error y exit(EXIT_FAILURE)"];
A --> E["2 - do_pipe()"];
E --> EA["pipe(): crea un canal de comunicación entre dos procesos."];
EA --> EAA["Si pipe() == -1"];
EAA --> C;
E --> EB["fork(): genera un nuevo proceso hijo a partir de un proceso padre."];
A --> G["3 - Fork: genera un nuevo proceso hijo a partir de un proceso padre."];
G --> H["Si fork() < 0"];
H --> C;
G --> I["Si fork() == 1: estamos en el proceso hijo"];
I --> J["***child_process***: configura y ejecuta el primer comando en el pipeline"];
G --> K["Si fork() > 1: estamos en el proceso padre"];
K --> L["***parent_process***: configura y ejecuta el último comando en el pipeline"];
J --> M["1 - Comprueba los accesos del archivo de entrada"];
M --> N["Si el archivo no existe o no tiene permisos de lectura"];
N --> Q["Mensaje error a ***handle_error***, ***close_pipefd*** y exit (EXIT_FAILURE)"];
J --> O["2 - Obtiene el fd del archivo de entrada con open()"];
O --> P["Si fd == -1"];
P --> Q;
J --> R["3 - Cierra el extremo de lectura (pipefd[0]), porque aquí no se usa"];
J --> S["4 - Redirige la stdin al fd del archivo de entrada y el stdout al extremo de escritura del pipe (pipefd[1])"];
J --> T["5 - Cierra el input original y el pipefd[1] después de la redirección"];
J --> U["6 - Llama a ***execute*** para runear el cmd1. El comando lee del stdin redirigido, es decir, del archivo de entrada, y escribe su output en el stdout redirigido, en pipefd[1]"];
L --> W["1 - waipid(): espera a que termine el proceos hijo"];
L --> X["2 - Abre o crea el archivo de salida con open(), con permisos de escritura y lectura. Si ya existe, lo trunca"];
L --> Y["3 - Comprueba los accesos del archivo de salida"];
Y --> Z["Si el archivo no tiene permisos de lectura"];
Z --> AF["Mensaje error a ***handle_error***, ***close pipefd*** y exit (EXIT_FAILURE)"];
X --> AA["Si fd == -1"];
AA --> AF;
L --> AB["4 - Cierra el extremo de escritura pipefd[1] porque aquí no se usa"];
L --> AC["5 - Redirige la stdin al extremo de lectura del pipe (pipefd[0]) y el stdout al fd del archivo de salida"];
L --> AD["6 - Cierra el pipefd[0] y el output original después de la redirección"];
L --> AE["7 - Llama a ***execute*** para runear el cmd2. El comando lee del extremo de lectura (pipefd[0]) y escribe su output en el archivo de salida"];
style C fill:#ffcccb,stroke:#ff0000,stroke-width:1px
style Q fill:#ffcccb,stroke:#ff0000,stroke-width:1px
style AF fill:#ffcccb,stroke:#ff0000,stroke-width:1px
graph LR;
A["***execute*** (pipex.c): "] --> B["Splitea el comando"];
B --> C["Si el split da NULL o el primer elemento del array es nulo"];
C --> D["***handle_cmd_error***: mensaje error y libera el array de argumentos. Si era el primer comando, cierra el stdout y exit (EXIT_FAILURE)"];
A --> E["***get_path***: obtiene el ejecutable para un comando."];
E --> F["***get_path_from_envp***: obtiene la ruta del array de la variable de entorno"];
F --> G["Busca el string de envp que empieza por PATH= y devuelve el resto de esa string o NULL si no la encuentra"];
E --> H["Splitea la ruta obtenida del ***get_path_from_envp*** para obtener los directorios, splitea el comando en sus argumentos y libera la ruta"];
H --> I["Si no hay directorios o no hay argumentos de comando, libera los arrays y devuelve NULL"];
E --> K["***build_cmd_path***: construye la ruta del comando"];
K --> L["Hace strjoin de cada directorio con / y con el primer argumento de comando. Si la ruta existe y tiene permisos de ejecución, libera los arrays y la devuelve. Si no, lo intenta con el siguiente directorio. Si no lo encuentra, devuelve NULL."];
E --> M["Si existe, devuelve el array resultante de ***build_cmd_path***, si no, libera los arrays y devuelve un duplicado del cmd"];
A --> N["Si la ruta obtenida de ***get_path*** es NULL o no tiene acceso de ejecución"];
N --> O["Libera la ruta"];
O --> D;
A --> P["Llama a execve() con la ruta, los argumentos del comando y la variable de entorno"];
P --> Q["Si execve() == -1"];
Q --> R["***handle_cmd_error***: mensaje error y libera el array de argumentos. Libera la ruta, los argumentos de comando y exit (EXIT_FAILURE)"];
style D fill:#ffcccb,stroke:#ff0000,stroke-width:1px
style R fill:#ffcccb,stroke:#ff0000,stroke-width:1px
graph LR;
A["***main (main_bonus.c)***"] --> B["***check_args*** (main.c): checkeo de los argumentos."];
B --> C["Si argc < 5"];
C --> D["Mensaje error y exit (EXIT_FAILURE)"];
B --> E["Si argv[1] es here_doc y argc != 6"];
E --> D;
A --> F["Si argv[1] es here_doc"];
F --> G["***here_doc*** (pipex_bonus.c): configura y ejecuta e here_doc en un proceso hijo"];
G --> H["Crea un pipe con ***do_pipe*** ()"];
H --> I["Si pipe() o fork() < 1"];
I --> Ñ["Mensaje error y exit (EXIT_FAILURE)"];
G --> J["En el proceso hijo cierra el prev_pipefd[0] (extremo de lectura) y utiliza ***get_next_line*** (./my_libft) para leer las líneas del stdin hasta el delimitador (argv[2]) y escribirlas en prev_pipefd[1] (el extremo de escritura)"];
G --> K["En el proceso padre cierra prev_pipefd[1] y redirecciona el stdin a a prev_pipefd[0] (extremo de lectura). Luego waitpid() para esperar a que termine el proceso hijo."];
A --> L["En el resto de los casos llamamos a ***first_process_infile*** (pipex_bonus)"];
L --> M["Hace exactamente lo mismo que ***child_process** (pipex.c) de la parte no bonus"];
A --> N["Después, ***process_cmds*** (main_bonus.c)"]
N --> O["Empieza por argv[2] o argv[3] dependiendo de si es here_doc o no. Entra en un bucle que hace pipes para cada argumento hasta el último."]
N --> P["Los argumentos intermedios los pasa a ***inter_process*** (con el prev_pipefd y el pipefd) y el último argumento a ***last_process*** con el último comando y el prev_pipefd"];
P --> Q["***inter_process*** (pipex_bonus.c)"];
Q --> R["Espera a que termine cualquier proceso hijo con waitpid(). Cierra los pipefd que no utiliza (el de lectura del pipe actual (pipefd[0]) y el de escritura de pipe previo (prev_pipefd[1])). Redirige stdin al extremo de lectura del pipe previo (prev_pipefd[0]), y redirige el stdout al extremo de escritura del pipe actual (pipefd[1]). Cierra los fds originales depsués de la redirección y llama a ***execute*** (pipex_bonus.c), que es igual al del pipex sin bonus."];
N --> S["***last_process*** (pipex_bonus.c): funciona exactamente igual que el ***parent_process*** del pipex sin bonus"];
N --> T["Al final del bucle, cierra el extremo de lectura del pipe previo (prev_pipefd[0]) y el extremo de escritura del pipe actual (pipefd[1]). Sustituye el pipe previo por el pipe actual."];
style D fill:#ffcccb,stroke:#ff0000,stroke-width:1px
style Ñ fill:#ffcccb,stroke:#ff0000,stroke-width:1px
Teoría y guías → AQUÍ, AQUÍ, AQUÍ
Tester → AQUÍ