Índice
./pipex archivo 1 comando 1 comando2 archivo2
$> < archivo1 comando1 | comando2 > archivo2
Básicamente hay que hacer que el primer comando actue como el segundo
El bonus tiene dos partes. La primera gestionar multiples pipes
$> ./pipex archivo1 comando1 comando2 comando3 ... comandon archivo2
$> < archivo1 comando1 | comando2 | comando3 ... | comandon > archivo2
Aceptar << y >> cuando el primer parametro es "here_doc":
$> ./pipex here\_doc LIMIRADOR comando comando1 archivo
comando << LIMITADOR | comando1 >> archivo
<infile
: Redirige el contenido de un archivo infile
como stdin
para el comando1.
Es importante entender que aunque un comando no use el stdin
para nada, por ejemplo ls
, todos los procesos en UNIX tienen su stdin
, stdout
, stederr
. Cuando estos no se establecen se toman por defecto, y cuando no se usan simplemente se ignoran. En el caso de este proyecto, es necesario que te den explicitamente los 4 argumentos, no tienen que coger ninguno por defecto.
|
: Toma el stdout
del comando1 como stdin
del comando2. Se encarga de la comunicación entre los dos comandos.
>outfile
Esto redirige el stdout
de comando2 a un archivo llamado outfile
(se sobreescribe o crea).
Nuestro amigo CodeVault explica como reclear un pipe en este video.
Es un proyecto no muy grande, pero que introduce un montón de conceptos nuevos. Esta lista de reproducción abarca todo lo necesario y más sobre los procesos de UNIX. Pero si quieres ir al grano y empaparte con las nuevas funciones, usa este orden:
-
exec
yerrno
: como llamar y ejecutar otros procesos dentro de tu programa y gestionar los errores. -
fork
:exec
sustituye el proceso actual por uno nuevo, por lo que pierdes todo lo que haya después deexec
... a no ser que dividas el proceso. Bienvenido a la gestión de procesos y cómo duplicarlos para hacer varias llamadas a otros programas. -
wait
ywaitpid
: ahora que tenemos procesos en paralelo, tenemos que aprender a ejecutarlos en el orden que queramos. -
pipe
: ahora que ya sabes todo lo necesario sobre procesos, vamos a ver manejo de file descriptors y cómo se comunican entre ellos. -
dup
ydup2
: ahora que ya tenemos todo lo necesario sobre procesos y demás, vamos a ver cómo duplicar un fd y cambiar STDOUT a un archivo en concreto.
Son números enteros que usa cada proceso para referirse a archivos.
Las descripciones de archivo abiertas son estrucuras internas del kernnel que contienen al informacións obre el archivo abierto como el offset y las flags de archivo (modo lectura/escritura).
Aunque distintos descriptores de archivo pueden apuntar al mismo open file description, luego tienen sus flags especificos, los llamadas file descriptor flags. Estas flags no perteneces a las descripciones de archivos así que no se comparten al hacer dup
, por ejemplo.
Un descriptor de archivo importante para pipex es FD_CLOEXEC (Close On Exec) que indica si el descriptor debe cerrarse automaticamente después de que se ejecute un proceso con execve
.
open
: en esta ocasión, vamos a utilizaropen
para crear el fichero en el que vamos a escribir. Para ello podemos usar la flagO_CREAT
, que permite crear un fichero desde 0 con los permisos necesarios, como0644
. También podemos usar un biwise|
conO_WRONLY
para escribir en el archivo si ya existe o crearlo si no. En resument, usaropen("somefile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644)
.close
read
write
: ahora tiene un giro, y es que en lugar de usar el fd 1 para escribir por pantalla, podemos escribir a otros fd. Util para redirigir output desde procesos diferentes.malloc
free
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
Ejecuta el programa referido por pathname
, que será la ruta al archivo ejecutable.
La familia de los exec
sirve para ejecutar un programa de C dentro de otro programa de C. P.e, ejecutar GNL como un ejecutable en lugar de una llamada a función. Esto significa que el programa siendo ejecutado por el porceso de llamado es remplazado por un nuevo programa, con una nuevo stack, heap y segmento de datos. Dicho de otra forma, se borra toda la info asociada al programa anterior y se inicializan las nuevas areas de memorias para el programa, (si todo va bien, claro).
Hay varias flags para utilizar exec
.
l
: lal
es de lista, indica que va a recibir una lista de argumentos. Es decir, que recibe los argumentos como unava_list
, y cada uno de ellos se especifica de manera individual. Sirve cuando sabemos los argumentos de antemano y los podemos escribir a mano.
Por ejemplo, podemos ejecutar ls
con las flags -l
y -a
int execl(const char *path, const char *arg0, ..., NULL);
execl("/bin/ls", "ls", "-l", "-a", NULL);
v
: lav
es de vector, lo que significa que en lugar de pasar una lista de argumentos definida, pasamos un array de strings. Se emplea cuando no sabemos los argumenos de antemano y queremos utilizar un ejecutable de manera dinamica, de forma parecida a como funciona[argv]
. Es excluyente al
.
int execv(const char *path, char *const argv[]);
char *args[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", args);
p
: lap
, comoexecvp
, indica que los ejecutables se encuentran en elPATH
del sistema o del entorno que estemos usando, por lo que ahorramos tiempo en definir elpathname
. Por ejemplo, para ls, en lugar de pasar "/bin/ls" como argumento, basta con indicar "ls", y el programa buscara el ejecutable en la carpeta del sistema.
char *args[] = {"ls", "-l", "-a", NULL};
execvp("ls", args);
e
: lae
es de environment o variables de entorno. Mientras que normalmente usamos argumentos para darle información a un programa, también podemos pasarle una variable de entorno, compartiendo la misma información (argumentos, estados del heap, etc.) entre varios programas. Es la versión más flexible, ya que permite customizar el entorno, dar los paths especificos de los programas que queremos ejecutar, etc. La sintaxis esKEY=VALUE
.
char *args[] = {"/bin/ls", "-l", "-a", NULL}; // Argumento
char *env[] = {"HOME=/home/user", "PATH=/custom/bin", NULL}; // Variable de entorno modificada, ahora solo busca programas es en esa carpeta.
execve("/bin/ls", args, env);
Como vemos, hay diferentes tipos de exec
, como execv
o execvp
, cada uno con sus diferencias. En este caso nos vamos a centrar en execve
, aqui va el resumen.
-
pathname
es la ruta del archivo a ejecutar. Debe ser un ejecutable binario, o un script con simbolos(!#) como los de bash -
argv
es un array de string con los argumentos por linea de comandos del programa. Aunque no es obligatorio normalmente el primer valor es el nombre del programa. Además este array debe acabar conNULL
. Vamos, que argv[argc] esNULL
. -
envp
es un array de strings, convencionalmente de la forma"key=value"
, que contiene las variables de entorno que el programa usará. También debe acabar enNULL
.
Un ejemplo de implementación:
char argv[] = {
"./gnl", // Hay que pillar el pathname y la ruta hasta que lo pillas y lo devuelves. Copiar del 0 al 1, y después del 1 al 0 para sacarlo
"Input for GNL", // Primer argumento, que es igual que el pathname. Aqui hay que sacar el comando
NULL // Señalamos el final de los argumentos
};
execve(
"./gnl", // Pathname del archivo a ejecutar
argv
);
Por último si execve falla, retornará -1 y establecera errno
(más a continuación) con el error especifico para que se pueda manejar el error.
Para más info de la familia exec, este vídeo
Hemos dicho que si todo va bien, exec salta al nuevo proceso directamente, ignorando todo lo que hay después. Pero, ¿cómo gestionamos los casos en los que dé error? Aquí entra errno
, strerror
y perror
.
#include <errno.h>
int errno;
errno
(error number) muestra un código de error de la última llamada al sistema o función a la librería. Cuando una llamada al sistema falla, generalmente devuelve -1 y asigna a la variable errno un valor que describe qué salió mal. (Estos valores se pueden encontrar en <errno.h>
). Muchas funciones de la libreria hacen lo mismo. Por ejemplo:
char *args[] = {"/nonexistent/program", NULL}; // Nonexistent program
char *envp[] = {NULL}; // Empty environment variables
execve(args[0], args, envp); // This will fail...
int error = errno; // ...we get errno value;
printf("%d", error); // ..and the value of int error will be errno flag 2, which is ENOENT (Error NO ENTry).
Esto nos da un número, pero para leer el mensaje completo y tener algo legible, debemos usar strerror
(string error) o perror
(pointer error).
#include <stdio.h>
char *strerror(int errnum);
void perror(const char *s);
strerror
devuelve cadena que que describe el error de unerrno
, introducida como el argumentoerrnum
. Devuelve un mensaje por defecto y preestablecido. Al devolver la cadena, hay que imprimirla por pantalla con otra funcion, comoprintf
.
FILE *file = fopen("/nonexistent/file", "r");
if (!file)
printf("Error: %s\n", strerror(errno));
perror
imprime un mensaje personalizado seguido de la descripción del ultimo errorerrno
(number of last error) de una llamada al sistema o de una funcion de la libreria. El mensaje que se devuelve es escrito a mano por el usuario.
FILE *file = fopen("/nonexistent/file", "r");
if (!file)
perror("Custom error message");
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
Crea un nuevo proceso, denominado hijo duplicando el anterior, denominado padre (parent and child process). Devuelve un int que es un ID diferente para cada fork o -1 si ha habido un error. El de child siempre será 0, por lo que tenemos que usar este dato para crear variables de control y ver qué programas queremos ejecutar. Un ejemplo.
pid_t pid = fork();
if (pid < 0) // Error in fork
perror("fork");
else if (pid == 0) // Child process
printf("Child process\n");
else // Parent process
printf("Parent process\n");
Ambos procesos corren en espacios de memoria separados, por lo que genera un nuevo proceso con el que podemos jugar, muy util para las llamadas a exec
. Ten en cuenta que fork
se crea desde la línea que se llama, por lo que sucesivas llamadas a fork
irán creando diferente ramificaciones en un ratio de 2^n, un arbol binario. Si quieres más, aquí más info sobre cómo funciona fork
.
#include <stdlib.h>
void exit(int status);
Termina el proceso actual y cierra todos los fd. Útil para la gestión de errores de fork
o para controlar sus flujos. Admite dos valores como argumento de entrada:
-
0 o EXIT_SUCCESS para indicar que el proceso se cerró según lo esperado.
-
1 o EXIT_FAILURE para indicar que el proceso se cerró debido a un error.
Siguiendo con el ejemplo anterior:
pid_t pid = fork();
if (pid < 0) // Error in fork
{
perror("fork");
exit(1); // También valdría exit(EXIT_FAILURE)
}
else if (pid == 0) // Child process
printf("Child process\n");
else // Parent process
printf("Parent process\n");
Aunque no se va a usar en este proyecto, abort
hace lo mismo pero, a diferencia de fork
, no cierra los fd. Esto es importante porque, en caso de dump core, exit
puede llegar a corromper datos si se mete donde no debe.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
Ahora que tenemos varios procesos, estaría bien elegir en qué orden se van a ejecutar. Si llamamos fork directamente, ambos procesos se van a ejecutar de manera simultanea, lo que puede ser un problema. Por ejemplo, se puede intercalar un printf de un proceso con el otro, mostrando ambos resultados por pantalla a la vez.
wait
para la ejecución de parent hasta que termine la ejecución de child, lo cual nos ayuda a escoger el orden de los procesos. wait
por si misma va a esperar a que acabe el child que está activo en el momento, pero puede ser un problema si llamamos a wait
desde el propio child sin otra llamada a fork
, porque se va a quedar esperando hasta el infinito. Una solución fácil.
pid_t id = fork();
if (id != 0)
wait(NULL);
wait
devuelve como valor el id del proceso por el que ha esperado y -1 si no había ningún proceso child por el que esperar, también útil para la gestión de los procesos, tal que así:
pid_t id = fork();
int wait_id;
wait_id = wait(NULL);
if (wait_id == -1)
printf("No children to wait");
else
printf("%d finished execution", wait_id);
También podemos controlar el fin de un proceso con errno
y la flag ECHILD
while (wait(NULL) != -1 || errno != ECHILD)
printf("waited for a child to finish");
Pero este metodo es demasiado rudimentario para cuando tenemos varios procesos a la vez, ya que wait
espera al primer child que termina de procesarse, no discrimina cual. Ya que no podemos usar getpid
ni getppid
(que nos especificarían los id de cada proceso y lo podriamos usar como condición), para solucionarlo tenemos waitpid
, una variante de wait
en la que podemos especificar un id en concreto y que hasta que cambio de estado podemos esperar. Sus argumentos son:
-
pid
: la id del proceso en concreto al que queremos esperar, obtenido conint id = fork()
. Tiene varios valores de entrada.- pid > 0: espera al id de un children específico.
- pid == 0: espera a por cualquier children del mismo grupo del padre.
- pid == -1: espera a cualquier children, independientemente del grupo, al igual que el
wait
original. - pid < -1: espera a que termine cualquier children del mismo grupo que el padre.
-
wstatus
: el cambio de estatus del proceso al que estamos esperando: el children ha terminado, ha sido parado o a reanudado. Se pueden consultar las flags en<sys/wait.h>
y poner NULL si no se quiere marcar nada, pero algunos comunes.-
WIFEXITED: devuelve distinto de 0 si children ha terminado con normalidad, como retorno main o exit.
-
WIFEXITSTATUS: devuleve código de salida de children si WIFEXITED es verdadero.
-
WIFSIGNALED: devuelve un valor si children acabó por una señal como SIGTERM o SIGKILL
-
WIFSTOPPED: devuelve un número si children se ha detenido con señales como SIGSTOP.
-
-
options
: opciones para modificar el comportamiento dewaitpid
cuando termine children, se puede usar 0 para no marcar nada o algunas de estas flags.-
WNOHANG: No hang, no bloquea el programa en caso de que no haya children que hayan cambiado de estado. Es decir, primero verifica si algún hijo ha cambiado de estado, y si no es así retorna 0 y continua con el programa. Es la más interesante de usar, ya que se puede emplear como un simple check de si un proceso ha terminado o no.
-
WUNTRACED: continua con el programa en caso de que un children se detenga con alguna señal, como SIGSTOP o SIGSTP, que se envian cuando se pulsa Ctrl+z desde la terminal.
-
WCONTINUED: continua con el programa en caso de que se reanude un children con señales como SIGCONT.
-
#include <unistd.h>
int pipe(int pipefd[2]);
Crea un canal de comunicacióon unidireccional para dos extremos. Uno de lectura y otros de escritura. Necesita como input un array de dos int que contenga el fd de donde va a leer y el fd de donde va a sacar esa lectura. Podemos pensarlo igual que stdin y stdout, donde 0 es lectura y 1 es escritura.
int fd[2];
// fd[0] Where to read;
// fd[1] Where to write;
En teoria es muy parecido a la funcion read
, donde generabamos un fd solo de lectura a partir de un fichero determinado. Con pipe
, lo que hacemos es conectar dos fd diferentes, uno de lectura (0) y otro de escritura (1);
Para empezar, deberíamos comprobrar que el pipe se ha abierto correctamente, devuelve -1 en caso de error.
int fd[2];
if (pipe(fd) == -1)
printf("error");
Una vez que tenemos el pipe abierto, podemos decirle de escribir en uno de los fd que hemos abierto a otro fd, con llamadas al sistema como write
. Un ejemplo.
int fd[2]; // Abrimos array
int x; // Int en desde el que vamos a escribrir
int y; // Int al que vamos a escribrir a traves del pipe
if (pipe(fd) == -1) // Lanzamos pipe y vemos que abra bien
{
printf("error");
return (1);
}
x = 2; // Asignamos valor a x...
write (fd[1], &x, sizeof(int)); //... y con el pipe se lo pasamos al fd[0].
close (fd[1]);// Cerramos uno de los extremos del pipe, ya que no lo vamos a usar.
read(fd[0], &y, sizeof(int)); // Asignamos el 2 de x en y leyendo a través de fd[0], gracias al pipe
close(fd[0]); // Y cerramos el pipe
printf("%d", y); // Imprimimos 2 a traves de la variable y.
Una vez abierto el pipe
, no hace falta que tengamos que usar los dos extremos. Puede que solo queramos escribir en uno de los fd, por lo que podemos lanzar un close
para el fd que no usemos.
Este manejo de pirpe
es util en multiprocesos. Por ejemplos, en un proceso podemos hacer un write desde fd[1] a fd[0], y en el parent leer ese mismo fd. A la hora de hacer un fork
, hay que tener en cuenta que el pipe se mantiene independiente. Es decir, aunque cerremos el pipe
en un proceso children, sigue abierto en el parent. Por lo que es importante tenerlos vigilados para no dejar nada sin cerrar.
int fd[2]; // Abrimos array
int x; // Int en desde el que vamos a escribrir
int y; // Int al que vamos a escribrir a traves del pipe
int fork_id; // Variable para el id fork y control de procesos
if (pipe(fd) == -1) // Lanzamos pipe y vemos que abra bien
{
perror("pipe error");
return(1);
}
fork_id = fork(); // Dividimos el proceso en parent y child
if (fork_id == -1) // Variable de control para el fork
{
perror("fork error");
exit(1);
}
if (fork_id == 0) // Entramos en children para asignar el valor a x
{
close(fd[0]); // Cerramos fd[0] ya que no lo vamos a usar en este proceso
x = 2;
write (fd[1], &x, sizeof(int)); // Hacemos el write en fd[1]...
close(fd[1]); // ... y lo cerramos;
}
else // En el parent leemos el valor de x en y
{
close (fd[1]); // Cerramos fd[1] ya que no vamos a escribir
read (fd[0], &y, sizeof(int)); // Leemos el 2 del fd[0] en y
close (fd[0]); // Cerramos el read
printf("%d\n", y); // Y nuestro y vale 2!
}
int access(const char *pathname, int mode);
"Check user's permissions for a file"
Verifica que un programa puede acceder al archivo marcado por pathname, mientras que mode especifica que tipo de permiso (lectura, escritura, ejecución...) tiene el archivo. Se puede usar un bitwise OR o AND para comprobar varios valores.
En caso de éxito, devuelve 0. En error, -1.
Una implementación podría ser:
if (access("rwfile", R_OK|W_OK) == 0)
printf("rwfile is accessible in writing and reading mode\n");
La idea es llamarla antes de execve
para comprobar si un archivo existe y es ejecutable. Lo mismo aplica a los ejectuables de PATH, tenemos que comprobar que existe el comando que queremos ejecutar y que ejecutable.
Para sacar el resultado de nuestro programa a un archivo en concreto en lugar de una terminal, necesitamos usar dup
y dup2
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
"Duplicate a file descriptor"
dup
crea una copia de cualquier fd y lo asigna al usando el número de fd más bajo disponible. Es decir, que si un open
un fd de 4, podemos duplicarlo al 5 y hacer otras cosas con el. Por ejemplo.
int fd = open("example.txt", O_WRONLY | O_CREAT 0644); // Abrimos un archivo y le asignamos un fd
int fd_dup = dup(fd); // Duplicamos el fd y escribimos en el
write(fd, "Hello, ", 7);
write(fd_dup, "world!\n", 7); // Pero tambien podemos escribir en su copia
// Al acabar, example.txt contiene "Hello, world!"
Pero la chicha viene con dup2
, ya que nos permite copiar un fd y asignarlo a cualquier otro fd, incluido STDOUT
. Repito, incluido STDOUT
. Por lo que si le asignamos 1 a nuestro fichero, cualquier escritura en STDOUT
se realizará dentro del fichero que queramos, super útil para el pipex. Por ejemplo.
int fd = open("output.txt", O_WRONLY | O_CREAT 0644); // Abrimos un fd
dup2(fd, STDOUT_FILENO) // Y sustituimos STDOUT por ese archivo
printf("Este mensaje se guarda en el archivo.\n");
printf("Cualquier otra salida de printf también irá al archivo.\n");
// Al acabar, output.txt contiene "Este mensaje se guarda en el archivo. Cualquier otra salida de printf también irá al archivo."
Si oldfd
no es valido, la llamada falla y newfd
no se cierra. Si oldfd
es un descriptor valido y tiene el mismo valor que newfd
no se hace nada y se devuelve newfd
. Si newfd
ya estaba siendo usado se cierra antes de reusarse (cuidado, porque lo hace sin avisar).
Tras ejecutarse correctamente ambos file descriptors se refieren al mismo file description, es decir tienen las mismas status flags y el mismo offset.
Sin embargo ambos file descriptors no comparten las file descriptors flags. En este sentido el FD_CLOEXEC (que señala que un file descriptor se debe cerrar cuando se realiza un exec
) es off. Es decir, que el file descriptor sobrevivirá en el nuevo programa aún después de exec
. Para mas info, el manual.
El proceso de cerrarse y reusarse se realizaría atomicamente
, esto es importatnte, porque implementar la misma fucnionalidad con close y dup estaría sujeto a rece condition, ya que newfd podría ser reusado entre los dos pasos (los hilos comparten la memoria de un proceso y el mismo fd).
#include <unistd.h>
int unlink(const char *pathname);
En caso de que algo falle, debemos borrar el archivo que hemos creado y en el que hemos intentado escribir. Normalmente usariamos remove
, pero en este caso usaremos unlink
. unlink
Elimina la entrada del directorio marcada por pathname
, y su contendio en caso de que ningún proceso esté haciendo uso del archivo. Un ejemplo de uso.
// Abrir el archivo directamente
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
// Redirigir stdout al archivo
dup2(fd, STDOUT_FILENO);
// Escribir en el archivo usando printf
printf("Este texto se escribe en example.txt.\n");
printf("Es un ejemplo sin gestión de errores.\n");
// Cerrar el archivo
close(fd);
// Eliminar el archivo
unlink("example.txt");
Devuelve 0 en caso de exito y -1 en caso de error, estableciendo la flag correspondiente de errno
.
EACCES
: Permiso denegado para eliminar el archivo.ENOENT
: El archivo no existe.EPERM
oEISDIR
: Intento de usar unlink en un directorio (no permitido para directorios).
Ahora que ya hemos visto las funciones principales de nuestro pipex, vamos a realizar una implementación básica. Pongamos que queremos replicar el siguiente comando.
ping -c 5 google.com | grep rtt
Vamos a entender un poco el proceso que seguiría este comando.
- El comando
ping
lanza una llamada a Google y escribe la respuesta en STDOUT - En lugar de pasar por STDOUT, el simbolo
|
recoge esa salida y la pasa al siguiente comando,grep
grep
, en lugar de leer de STDINT, lee del texto que le ha pasado|
Ahora, para recrear ese proceso en C, debemos hacer el siguiente orden.
- Crear un
pipe
con dos fd diferentes, uno de lectura y otro de escritura. - Creamos dos procesos (child) diferentes
- Uno que ejecute el comando
ping
y escribe en fd[1] en lugar de STDOUT. - Otro que ejecute el comando
grep
y lea de fd[0] en lugar de STDIN.
- Uno que ejecute el comando
- Un proceso final que espere a que los otros dos child se ejecuten en el orden correcto y cierre el parent.
Aqui hay un ejemplo con execlp
(recuerda que debemos usar execve
).
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
int fd[2]; // Creamos el pipe y comprobamos el caso de error
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
int pid1 = fork(); // Creamos el primer fork para `ping` y comprobamos el error
if (pid1 < 0) {
perror("fork pid1");
return 2;
}
if (pid1 == 0) { // Proceso hijo 1: ejecuta `ping`
close(fd[0]); // Cerramos el extremo de lectura del pipe
dup2(fd[1], STDOUT_FILENO); // Redirigimos la salida estándar al extremo de escritura del pipe
close(fd[1]); // Cerramos el extremo de escritura duplicado
execlp("ping", "ping", "-c", "5", "google.com", NULL); // Ejecutamos `ping`
perror("execlp ping"); // Si execlp falla
exit(1); // Terminamos el hijo con error
}
int pid2 = fork(); // Creamos el segundo fork para `grep` y comprobamos el error
if (pid2 < 0) {
perror("fork pid2");
return 3;
}
if (pid2 == 0) { // Proceso hijo 2: ejecuta `grep`
close(fd[1]); // Cerramos el extremo de escritura del pipe
dup2(fd[0], STDIN_FILENO); // Redirigimos la entrada estándar al extremo de lectura del pipe
close(fd[0]); // Cerramos el extremo de lectura duplicado
execlp("grep", "grep", "rtt", NULL); // Ejecutamos `grep`
perror("execlp grep"); // Si execlp falla
exit(1); // Terminamos el hijo con error
}
// En el proceso padre cerramos ambos extremos del pipe
close(fd[0]);
close(fd[1]);
// Esperamos a que ambos procesos hijos terminen
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0; // Todo ha salido bien
}
Ahora, si queremos lanzar este programa con un argumento desde la consola, debemos añadir las funcionalidades de open
y tener en cuenta los argumentos (una vez más, este ejemplo usa execlp
en lugar de execve
).
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd[2]; // Pipe entre los dos comandos
int file_in, file_out; // Descriptores para archivo1 y archivo2
int pid1, pid2;
// Verificar número de argumentos
if (argc != 5)
{
write(2, "Usage: ./pipex file1 cmd1 cmd2 file2\n", 36);
return (1);
}
// Abrir archivo1 (modo lectura) y archivo2 (modo escritura)
file_in = open(argv[1], O_RDONLY);
if (file_in < 0)
{
perror("Error opening file1");
return (1);
}
file_out = open(argv[4], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (file_out < 0)
{
perror("Error opening file2");
close(file_in);
return (1);
}
// Crear el pipe y verificar errores
if (pipe(fd) == -1)
{
perror("Pipe failed");
close(file_in);
close(file_out);
return (1);
}
// Primer fork para ejecutar comando1
pid1 = fork();
if (pid1 < 0)
{
perror("Fork failed");
close(file_in);
close(file_out);
return (1);
}
if (pid1 == 0) // Proceso hijo 1
{
dup2(file_in, STDIN_FILENO); // Redirigir STDIN desde archivo1
dup2(fd[1], STDOUT_FILENO); // Redirigir STDOUT al pipe de escritura
close(fd[0]); // Cerrar el extremo de lectura del pipe
close(fd[1]);
close(file_in);
close(file_out);
execlp("/bin/sh", "sh", "-c", argv[2], NULL); // Ejecutar comando1
perror("Error executing cmd1");
exit(1);
}
// Segundo fork para ejecutar comando2
pid2 = fork();
if (pid2 < 0)
{
perror("Fork failed");
close(file_in);
close(file_out);
return (1);
}
if (pid2 == 0) // Proceso hijo 2
{
dup2(fd[0], STDIN_FILENO); // Redirigir STDIN desde el pipe de lectura
dup2(file_out, STDOUT_FILENO); // Redirigir STDOUT hacia archivo2
close(fd[0]);
close(fd[1]);
close(file_in);
close(file_out);
execlp("/bin/sh", "sh", "-c", argv[3], NULL); // Ejecutar comando2
perror("Error executing cmd2");
exit(1);
}
// Código del proceso padre
close(fd[0]); // Cerrar extremos del pipe
close(fd[1]);
close(file_in); // Cerrar archivo1
close(file_out); // Cerrar archivo2
waitpid(pid1, NULL, 0); // Esperar a que termine el primer hijo
waitpid(pid2, NULL, 0); // Esperar a que termine el segundo hijo
return (0); // Todo ha salido bien
}