Salta el contingut
 

Comunicació entre processos

Autor: Joan Puigcerver Ibáñez

Correu electrònic: j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Advertència

Aquest material està realitzat per a sistemes Linux.

En sistemes Windows, podeu executar els exemples mitjançant Windows System for Linux.

ProcessBuilder pb = new ProcessBuilder({"wsl.exe", "program", "arg1", "arg"});

Flux de dades estàndard

Els fluxos de dades estàndard son uns canals de comunicació interconnectats entre un programa i el seu entorn en moment d'execució. Hi ha tres canals d'E/S: l'entrada estàndard (standard input o stdin), l'eixida estàndard (standard output o stdout) i l'eixida d'error (standard error o (stderr).

Aquests fluxos estan connectats a la consola del sistema. Quan un programa s'executa mitjançant una shell interactiva, aquests canals estan connectats a la terminal, però poden ser redirigits a altres fitxers o altres processos.

  • Entrada estàndard (stdin): És un flux de dades d'on el programa pot rebre i llegir dades d'entrada.

    Si no ha segut redirigit, aquest flux rep les dades des del teclat.

    En Java, aquest flux de dades està associat a System.in i pot ser llegit mitjançant la classe Scanner.

    Scanner in = new Scanner(System.in);
    while(in.hasNextLine()){
      String data = in.nextLine();
    }
    
  • Eixida estàndard (stdout): És un flux de dades on el programa pot escriure les dades d'eixida.

    Si no ha segut redirigit, aquest flux mostra les dades per la terminal.

    En Java, aquest flux de dades està associat a System.out i pot escriure dades utilitzant els seus mètodes, com print o println.

    System.out.println("Dades que s'escriuren a STDOUT");
    
  • Eixida d'error (stderr): És un flux de dades on el programa pot escriure les dades d'error.

    Si no ha segut redirigit, aquest flux també mostra les dades per la terminal.

    En Java, aquest flux de dades està associat a System.err i pot escriure dades utilitzant els seus mètodes, com print o println.

    System.err.println("Dades que s'escriuren a STDERR");
    

Redirecció fluxos de dades

Els fluxos de dades treballen per defecte amb el teclat (stdin) o la terminal (stdout i stderr). No obstant això, aquests fluxos es poder redirigir cap a altres fonts utilitzant diferents operadors.

Operadors de redirecció

  • Redirecció eixida >: Crea un nou fitxer contenint l'eixida estàndard (no la d'error!) del programa executat. Atenció, si el fitxer existeix, aquest és sobreescrit (i es perden les dades existents)

    Redirecció d'eixida en Bash

    jpuigcerver@fp:~/psp $ ls
    jpuigcerver@fp:~/psp $ echo "Aquesta informació s'imprimeix per stdout"
    Aquesta informació s'imprimeix per stdout
    jpuigcerver@fp:~/psp $ echo "Aquesta informació s'imprimeix per stdout" > stdout.txt
    jpuigcerver@fp:~/psp $ ls
    stdout.txt
    jpuigcerver@fp:~/psp $ cat stdout.txt
    Aquesta informació s'imprimeix per stdout
    

    Aquest exemple comença amb la comanda ls, que mostra els continguts del directori actual, on es pot veure que està buit.

    Després, s'executa echo, que funciona com un print i mostra per pantalla (stdout) la informació:

    Aquesta informació s'imprimeix per stdout"
    

    Després es torna a executar, redirigint l'eixida al fitxer stdout.txt amb l'operador de redirecció >.

    Finalment, es mostra el contingut del fitxer stdout.txt, amb la informació anterior.

  • Redirecció afegir eixida >>: Afegeix (append) un fitxer l'eixida estàndard (no la d'error!). Si el fitxer no existeix es crea. Les noves dades s'afegeixen al final del fitxer i no s'eliminen les dades existents.

    Redirecció d'afegir eixida en Bash

    jpuigcerver@fp:~/psp $ cat stdout.txt
    Aquesta informació s'imprimeix per stdout
    jpuigcerver@fp:~/psp $ echo "Aquesta informació s'afegeix al final" >> stdout.txt
    jpuigcerver@fp:~/psp $ cat stdout.txt
    Aquesta informació s'imprimeix per stdout
    Aquesta informació s'afegeix al final
    

    Aquest exemple comença a partir del exemple anterior, on es mostra el contingut del fitxer stdout.txt.

    Després, s'utilitza la comanda echo afegir una informació al final del fitxer stdout.txt mitjançant la redirecció >>.

    Finalment, es mostra el contingut del fitxer stdout.txt, amb la nova informació al final del fitxer.

  • Redirecció error 2>: Crea un nou fitxer contenint l'eixida d'error del programa executat. Atenció, si el fitxer existeix, aquest és sobreescrit (i es perden les dades existents)

    Redirecció d'error en Bash

    jpuigcerver@fp:~/psp $ ls
    jpuigcerver@fp:~/psp $ rm inexistent
    rm: cannot remove 'inexistent': No such file or directory
    jpuigcerver@fp:~/psp $ rm inexistent 2> stderr.txt
    jpuigcerver@fp:~/psp $ ls
    stderr.txt
    jpuigcerver@fp:~/psp $ cat stderr.txt
    rm: cannot remove 'inexistent': No such file or directory
    

    Aquest exemple comença amb la comanda ls, que mostra els continguts del directori actual, on es pot veure que està buit.

    Després, s'executa rm, que intenta eliminar un fitxer anomenat "inexistent", el qual no existeix. Com es veu en la imatge, aquesta comanda falla i mostra per pantalla l'error (stderr).

    Es torna a repetir la comanda anterior, però esta vegada redirigint l'error al fitxer stderr.txt amb l'operador 2>.

    Finalment, es mostra el contingut del fitxer stderr.txt, amb l'error anterior.

  • Redirecció afegir error 2>>: Afegeix (append) un fitxer l'eixida d'error. Si el fitxer no existeix es crea. Les noves dades s'afegeixen al final del fitxer i no s'eliminen les dades existents.

    Redirecció d'afegir error en Bash

    jpuigcerver@fp:~/psp $ cat stderr.txt
    rm: cannot remove 'inexistent': No such file or directory
    jpuigcerver@fp:~/psp $ cd noexisteix
    bash: cd: noexisteix: No such file or directory
    jpuigcerver@fp:~/psp $ cd noexisteix 2>> stderr.txt
    jpuigcerver@fp:~/psp $ cat stderr.txt
    rm: cannot remove 'inexistent': No such file or directory
    bash: cd: noexisteix: No such file or directory
    

    Aquest exemple comença a partir del exemple anterior, on es mostra el contingut del fitxer stderr.txt.

    Després, s'utilitza la comanda cd, que intenta accedir al directori noexisteix, el qual no existeix. Com es veu en la imatge, aquesta comanda falla i mostra per pantalla l'error (stderr).

    Es torna a repetir la comanda anterior, però esta vegada redirigint l'error al final del fitxer stderr.txt amb l'operador 2>>.

    Finalment, es mostra el contingut del fitxer stdout.txt, amb el nou error al final del fitxer.

  • Redirecció eixida i error &> i &>>: Funciona de la mateixa manera que els operadors anteriors, amb la diferència que redirigeix els dos fluxos, el d'eixida i el d'error.

    Redirecció d'eixida i error en Bash

    jpuigcerver@fp:~/psp $ ls
    jpuigcerver@fp:~/psp $ echo "Aquesta informació s'imprimeix per stdout" &> out.txt
    jpuigcerver@fp:~/psp $ rm inexistent &>> out.txt
    jpuigcerver@fp:~/psp $ cat out.txt
    Aquesta informació s'imprimeix per stdout
    rm: cannot remove 'inexistent': No such file or directory
    

    Aquest exemple comença amb la comanda ls, que mostra els continguts del directori actual, on es pot veure que està buit.

    Després, s'executa echo, on es redirigeix l'eixida (stdout) al fitxer out.txt mitjançant l'operador &>.

    A continuació, s'executa la comanda rm inexistent, on l'error d'aquesta també es redirigeix al fitxer out.txt. En aquest cas, es gasta l'operador &>> per afegir-ho al final.

    Finalment, es mostra el contingut del fitxer out.txt, on es pot observar que els operadors &> i &>> redirigeixen els dos fluxos: stdout i stderr.

  • Redirecció entrada <: Passa les dades d'un fitxer com a entrada estàndard del programa.

    Redirecció d'entrada en Bash

    jpuigcerver@fp:~/psp $ cat text.txt
    Aquest es un text qualsevol
    jpuigcerver@fp:~/psp $ tr aeiou AEIOU < text.txt
    AqUEst Es Un tExt qUAlsevOl
    

    Aquest exemple comença amb un fitxer de text que conté un text qualsevol.

    Després, s'executa tr aeiou AEIOU, que tradueix totes les vocals minúscules a vocals majúscules. Aquesta comanda pot rebre la informació des de l'entrada estàndard (stdin). En aquest cas, li la passem des del fitxer text.txt mitjançant la redirecció <.

Canonades (pipes)

Les canonades (o pipes) són un mecanisme per a la comunicació entre processos utilitzant els fluxos de dades estàndard, de tal manera que el stdout de cada process es passa al procés següent com a stdin.

Les canonades es representen mitjançant el símbol |.

Exemple de canonades entre processos

Autor desconegut

Figura 1. Exemple de canonades entre processos

Canonades en bash
  • Mostra els colors del fitxer colors.txt per stdout.

    jpuigcerver@fp:~/psp $ cat colors.txt
    blue
    black
    red
    red
    green
    blue
    green
    red
    red
    blue
    

  • Ordena els colors per ordre alfabètic amb sort.

    jpuigcerver@fp:~/psp $ cat colors.txt | sort
    black
    blue
    blue
    blue
    green
    green
    red
    red
    red
    red
    

  • Compta les repeticions de cada color amb uniq -c.

    jpuigcerver@fp:~/psp $ cat colors.txt | sort | uniq -c
          1 black
          3 blue
          2 green
          4 red
    

  • Ordena els colors per ordre numèric invers amb sort -r -n.

    jpuigcerver@fp:~/psp $ cat colors.txt | sort | uniq -c | sort -r -n
          4 red
          3 blue
          2 green
          1 black
    

  • Mostra els tres colors més repetits; les tres primeres línies amb head -n 3.

    jpuigcerver@fp:~/psp $ cat colors.txt | sort | uniq -c | sort -r -n | head -n 3
          4 red
          3 blue
          2 green
    

  • Emmagatzema el color amb més repeticions en el fitxer favcolors.txt.

    jpuigcerver@fp:~/psp $ cat colors.txt | sort | uniq -c | sort -r -n | head -n 3 > favcolors.txt
    jpuigcerver@fp:~/psp $ cat favcolors.txt
          4 red
          3 blue
          2 green
    

Fluxos de dades en Java

Quan un programa Java crea un procés fill, el procés pare pot gestionar els fluxos de dades del fill per poder comunicar-se amb ell o que aquest es comunique amb altres processos.

Per fer-ho, s'utilitzarà la classe Process, que permet obtindre un objecte diferent per cada flux de dades:

Esquema de comunicació entre processos

Figura 2. Esquema de comunicació entre processos

Tots aquests objectes Stream, llegeixen i escriuen les dades en binari. Per facilitar la gestió d'aquestes dades, existeixen les classes InputStreamReader i OutputStreamWriter que treballen en caràcters en compte de bytes.

A més, aquests objectes poden ser combinats amb buffers, que emmagatzema les dades de manera temporal i ens proporciona mètodes per facilitar la gestió d'aquestes dades. BufferedReader per llegir les dades i BufferedWriter per escriure'n.

També existeix la classe PrintWriter, que permet escriure en un Stream amb mètodes similars als que ja coneixem: print, println, printf,...

Exemple: RunProcessOutput

Aquest programa crea un nou procés utilitzant l'ordre definida en la variable String[] program.

El procés és creat mitjançant la classe ProcessBuilder. Una vegada creat, el procés pare espera a que acabe el procés fill i imprimeix per pantalla el codi de retorn, l'eixida estàndard (stdout) i l'error (stderr) que ha produït.

RunProcessOutput.java
package ud1.examples;

import java.io.IOException;
import java.util.Arrays;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class RunProcessOutput {
    public static void main (String[] args) {
        String[] program = {"wsl", "echo", "Hello world!"};
        // String[] program = {"wsl", "rm", "inexistent"};
        ProcessBuilder pb = new ProcessBuilder(program);
        try {
            Process process = pb.start();
            // Objectes per poder llegir l'eixida estàndard i l'error
            BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
            BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));

            int codiRetorn = process.waitFor();
            System.out.println("L'execució de "+ Arrays.toString(program) +" ha acabat amb el codi: "+ codiRetorn);

            String line;
            System.out.println("Stdout:");
            while ((line = stdout.readLine()) != null)
                System.out.printf("    %s\n", line);

            System.out.println("Stderr:");
            while ((line = stderr.readLine()) != null)
                System.out.printf("    %s\n", line);

        } catch (IOException ex) {
            System.err.println("Excepció d'E/S:");
            System.err.println(ex.getMessage());
            System.exit(-1);
        } catch (InterruptedException ex) {
            System.err.println("El procés fill ha finalitzat de manera incorrecta.");
            System.exit(-1);
        }
    }
}

Exemple d'execució

  • String[] program = {"wsl", "echo", "Hola món!"}: El programa RunProcessOutput executa la comanda echo "Hola món!", que mostra per pantalla el text "Hola món".
L'execució de [wsl, echo, Hola món!] ha acabat amb codi de retorn: 0
Stdout:
    Hola món!
Stderr:
  • String[] program = {"wsl", "ls", "~"}: El programa RunProcessOutput executa la comanda ls ~, que mostra els continguts del directori d'usuari.
L'execució de [wsl, ls, ~] ha acabat amb codi de retorn: 0
Stdout:
    Desktop
    Documents
    Downloads
    Music
    Pictures
    Videos
Stderr:
  • java RunProcessOutput rm inexistent: El programa RunProcessOutput executa la comanda rm inexistent, que intenta eliminar un fitxer que no existeix. Es pot veure a la imatge que mostra el mateix missatge d'error que la comanda original.
L'execució de [rm, inexistent] ha acabat amb codi de retorn: 1
Stdout:
Stderr:
    rm: cannot remove 'inexistent': No such file or directory

Exemple: RunProcessInput

Utilitzant l'esquema anterior, aquest programa, a més, li passa dades al procés fill mitjançant l'entrada estàndard (stdin).

El procés és creat mitjançant la classe ProcessBuilder. Una vegada creat, el procés pare llegeix des d'entrada estàndard (teclat) i li passa aquestes dades al procès fill com a entrada estàndard (stdin).

Quan s'introdueix una línia en blanc, espera a que el fill acabe i imprimeix per pantalla el codi de retorn, l'eixida estàndard i l'error que ha produït.

RunProcessInput.java
package ud1.examples;

import java.util.Arrays;
import java.util.Scanner;
import java.util.Locale;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class RunProcessInput {
    public static void main (String[] args) {
        String[] program = {"wsl", "tr", "a", "A"};
        ProcessBuilder pb = new ProcessBuilder(program);
        try {
            Process process = pb.start();

            // Objecte per poder llegir l'entrada estàndard del programa pare
            Scanner in = new Scanner(System.in).useLocale(Locale.US);

            // Objecte per poder escriure a l'entrada estàndard del procés fill
            PrintWriter stdin = new PrintWriter(process.getOutputStream());

            System.out.println("Stdin:");
            String line;
            while(!(line = in.nextLine()).isEmpty())
                stdin.println(line);

            // Quan acabem de introduir dades, cal tancar el stream
            stdin.flush();
            stdin.close();

            // Objectes per poder llegir l'eixida estàndard i l'error
            BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
            BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));

            int codiRetorn = process.waitFor();
            System.out.println("L'execució de "+ Arrays.toString(program) +" ha acabat amb el codi: "+ codiRetorn);

            System.out.println("Stdout:");
            while ((line = stdout.readLine()) != null)
                System.out.printf("    %s\n", line);

            System.out.println("Stderr:");
            while ((line = stderr.readLine()) != null)
                System.out.printf("    %s\n", line);


        } catch (IOException ex) {
            System.err.println("Excepció d'E/S.");
            System.err.println(ex.getMessage());
            System.exit(-1);
        } catch (InterruptedException ex) {
            System.err.println("El procés fill ha finalitzat de manera incorrecta.");
            System.exit(-1);
        }
    }
}

Exemple d'execució

  • String[] program = {"tr", "aeiou", "AEIOU"}: El programa RunProcessInput executa la comanda tr aeiou AEIOU, que traduirà totes les vocals minúscules del text que rep per entrada estàndard a vocals majúscules.

    Stdin:
    Aquest es un text qualsevol
    
    L'execució de [tr, aeiou, AEIOU] ha acabat amb codi de retorn: 0
    Stdout:
        AqUEst Es Un tExt qUAlsevOl
    Stderr:
    

Canonades en Java

Una canonada (o pipe) consisteix en encadenar multiples programes, de tal manera que la eixida estàndard d'un es passat al següent com a entrada estàndard.

Java permet realitzar aquesta operació utilitzant el mètode List<Process> startPipeline(List<ProcessBuilder> builders), que executa una llista de programes (List<ProcessBuilder>) on connecta tots els programes mitjançant canonades.

Aquest mètode retorna una llista de processos (List<Process>) què conté els objectes necessaris per controlar cada procés.

Exemple: RunPiplineColors

Aquest exemple realitzarà la canonada que es mostra a la Figura 1.

Exemple de canonades entre processos

Autor desconegut

Figura 3. Exemple de canonades entre processos

Important

El fitxer colors.txt ha d'estar situat en el directori <projecte>/files/ud1/.

RunPipelineColors.java
package ud1.examples;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.ArrayList;

public class RunPipelineColors {
    public static void main (String[] args) {

        ArrayList<ProcessBuilder> programs = new ArrayList<>();
        programs.add(new ProcessBuilder("wsl", "cat", "files/ud1/colors.txt"));
        programs.add(new ProcessBuilder("wsl", "sort"));
        programs.add(new ProcessBuilder("wsl", "uniq", "-c"));
        programs.add(new ProcessBuilder("wsl", "sort", "-r", "-n"));
        programs.add(new ProcessBuilder("wsl", "head", "-3"));

        try {
            List<Process> processes = ProcessBuilder.startPipeline(programs);

            for(Process p : processes)
                p.waitFor();

            Process last = processes.getLast(); // .get(processes.size() - 1);
            BufferedReader stdout = new BufferedReader(new InputStreamReader(last.getInputStream()));

            String line;
            System.out.println("Stdout:");
            while ((line = stdout.readLine()) != null)
                System.out.printf("    %s\n", line);

        } catch (IOException ex) {
            System.err.println("Excepció d'E/S.");
            System.out.println(ex.getMessage());
            System.exit(-1);
        } catch (InterruptedException ex) {
            System.err.println("El procés fill ha finalitzat de manera incorrecta.");
            System.exit(-1);
        }
    }
}

Resultat de l'execució

Stdout:
    4 red
    3 blue
    2 green

Bibliografia

Comentaris