Nombre: Escobar Benitez Maria Soledad
Padrón: 97877
Repositorio: Link GitHub!
Fecha de entrega: 03/11/2020
En este trabajo se implementará una versión simplificada de un verificador de programas eBPF que sólo verificará la existencia o no de bucles de código y/o instrucciones sin ejecutar.
Para realizar esto la idea es modelar el programa como un grafo dirigido: los nodos serán las instrucciones y las aristas serán los posibles saltos.
El verificador de eBPF deberá detectar ciclos o bucles en el código. Esto es saltos hacia instrucciones ya ejecutadas. Al garantizar la no existencia de ciclos se puede garantizar que el programa de eBPF no se podrá colgar. Si se modela un programa como un grafo dirigido, la detección de ciclos se reduce tan solo a una búsqueda Deep First Search o DFS.
DFS no solo detecta los ciclos sino que además, por la naturaleza del algoritmo, recorre todo el grafo visitando todos los nodos, o sea, todas las instrucciones. Si alguna instrucción no es visitada durante el DFS es porque no puede ser accedida y por ende es una instrucción sin usar que nunca sería ejecutada.
El verificador de programas deberá procesar los archivos pasados por línea de comandos y determinar si tienen ciclos y/o instrucciones sin usar. Deberá imprimir por pantalla el nombre de cada archivo procesado y un mensaje indicando los resultados linea a linea, con la salida ordenada alfabéticamente.
El verificador deberá procesar los archivos en paralelo con una serie de threads definidos por línea de comandos. Es requerimiento del trabajo práctico que se implemente un objeto que servirá de repositorio de los archivos a ser procesados. Cada hilo deberá pedir un archivo a este objeto, parsearlo, modelarlo y analizarlo para luego volver a pedir otro archivo y repetir la operación. Por el otro lado, los hilos deberán guardar los resultados a medida que los vayan obteniendo en un único objeto compartido. Solo cuando todos los archivos hayan sido procesados se imprimirán los resultados.
Para la resolución de este trabajo práctico se decidió pensar en una división por partes, de los diferentes procesos a realizarse a la hora de llevar a cabo la verificación de un programa.
Inicialmente se detectan 3 grandes partes del programa, el procesamiento del archivo bpf, la creación del grafo y su verificación y la implementación de threads. Luego se puede notar la existencia de dos objetos que serán compartidos por todos los threads a lo largo del programa, uno de ellos es el repositorio donde se guardaran los archivos, que se reciben al momento de iniciar la ejecución del programa, y el otro es un repositorio donde cada thread irá guardando el resultado de la verificación de cada archivo.
Al momento de comenzar a modelar la solución se comienza a detectar la necesidad de otras entidades a las cuales se les delegaran ciertos comportamientos a lo largo de la vida del programa.
Para comenzar con la primer parte se decide implementar una clase FileParser, la cual recibe el nombre de un archivo y lo procesa para recolectar la información que resulte relevante para nuestro programa, para que esta información sea presentada de manera más ordenada se decide además crear una clase LineMap que almacena la información relevante de cada linea del archivo, es así que el FileParser devuelve una instancia de la clase LineMap.
Para esta parte del programa se crea una clase Checker que se encarga de solicitar las rutas a los archivos, que se encuentran almacenadas en la única instancia existente de la clase FileRepository, que es una de las clases que se menciona será compartida por todos los threads, y luego llamar a las diferentes clases que intervienen en la verificación del archivo. Esta parte del programa está a su vez sub-divida en la creación del grafo y la verificación del mismo.
Para crear el grafo la clase Checker hace utilización de la clase
GraphGenerator, la cual recibe la ruta a un archivo y se encarga de crear
una instancia de la clase Graph a partir de este archivo, para hacer esto
llama al FileParser y el mismo le devuelve un LineMap, el cual el
GraphGenerator consultará para ir creando cada instancia de la clase Node,
que luego agregará la Graph.
Para verificar el grafo el Checker usa además una clase llamada Dfs, la cual se encargará de recorrer el grafo y detectar la existencia de ciclos o instrucciones sin usar.
Finalmente el Checker crea el mensaje de salida, de acuerdo al resultado arrojado por la instancia de la clase Dfs y se encarga de almacenarlo en el repositorio de salidas, el cual es una clase a la cual se ha llamado OutputRepository, que es una de las clases compartidas por todos los threads.
LLegada a esta parte de la resolución ya se cuenta con todas las clases ya mencionadas y además se cuenta con una clase CheckerProgram que será la encargada de ejecutar todo el programa en sí, este será nuestro thread principal. Desde esta clase se lanzarán los diferentes threads, donde cada thread será una instancia de la clase Checker, para que esto fuese posible se creó una clase llamada CheckerThread, de la cual heredara la clase Checker y le permitira a cada instancia comprotarse con un thread.
La clase CheckerProgram será la encargada de crear los repositorios compartidos, FileRepository y OutputRepository, y le pasará una referencia a los mismos a cada instancia de la clase Checker, logrando así que todos tengan acceso a los mismos, puedan pedir las rutas de los archivos y luego almacenar los resultados.
Una vez se hayan procesados todos los archivos, CheckerProgram hará un join de todos los hilos, para luego mostrar los resultados y finalizar liberando todos los recursos utilizados.
Como el enunciado los requiere, contamos con dos objetos compartidos por todos los threads, FileRepository y OutputRepository, para evitar corromper los resultados del verificador, o generar errores a la hora de procesar los archivos, se ha debido detectar cuáles son las secciones críticas, es decir, cuales son las operaciones que no pueden ser interrumpidas por un context switch, pudiendo provocar así alteraciones en los resultados. En la clase FileRepository, se han detectado que la petición de rutas de archivos y la agregación de las mismas son secciones críticas, pues si estas fuesen interrumpidas y se cambiara el estado del repositorio justo antes de obner la respuesta, esto podria generar un resultado completamente inesperado, lo mismo sucede con la clase OutputRepository, la cual tiene como secciones críticas a la operación de guardar las salidas y la de mostrar las mismas.
Para proteger estas seciones críticas se ha asignado un mutex para cada uno de estos repositorios y se ha hecho utilización de lock_guard para evitar que las operaciones sean interrumpidas, y además despreocuparse de tener que llamar a lock y unlock, pues lock_guard nos garantiza que esa sección estará resguarda desde el momento en que inicia su ejecución hasta el fin de la ejecución del método.
A continuación se muestra un esquema, a alto nivel, de como se ha interpretado el flujo del programa, de acuerdo a la resolución implementada.
Por otro lado se presenta el siguiente diagrama de clases que intenta detallar, de la mejor manera posible, las clases implementadas para la resolución del trabajo práctico y la relación existente entre ellas.
A continuación se detallará un poco más acerca de las interfaces implementadas para cada una de las clases creadas, explicacando brevemente el proqué de dicha implementación. Enn caso de buscar una explicación más detallada, se puede consultar la documentación existente en cada uno de los archivos de cabecera.
Esta clase es la encargada de llevar a cabo la ejecución del verificdor de archivos, la cual nos permite proveer al main una completa abstracción de toda lógica. La clase cuenta con los siguientes atributos:
La clase posee la siguiente interfaz:
class CheckerProgram {
private:
FileRepository file_repo;
OutputRepository output_repo;
void saveFiles(std::vector<std::string>& files);
public:
CheckerProgram();
~CheckerProgram();
int start(const char *threads_num, std::vector<std::string>& files);
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta es una de las clases compartidas que se encarga de alamacenar las rutas de los archivos a verificar. La clase cuenta con los siguientes atributos:
La clase posee la siguiente interfaz:
class FileRepository {
private:
std::queue<std::string> files;
std::mutex mutex;
public:
FileRepository();
~FileRepository();
void addFile(const std::string& file_path);
std::string getFile();
FileRepository(const FileRepository&);
FileRepository& operator=(const FileRepository&);
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta es la segunda clase compartida, es un repositorio encargado de almacenar las salidas correspondientes al resultado de las verificaciones de los archivos. La clase cuenta con los siguientes atributos:
La clase posee la siguiente interfaz:
class OutputRepository {
private:
std::set<std::string> outputs;
std::mutex mutex;
public:
OutputRepository();
~OutputRepository();
int addOutput(const std::string output);
void showOutput();
OutputRepository(const OutputRepository& other);
OutputRepository& operator=(const OutputRepository& other);
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta clase es la encargada de llevar a cabo al verificador de archivos, esto implica la creación del grafo y la verificación del mismo, para luego almacenar los resultados dentro del repositorio de salidas. La misma hereda de la clase CheckerThread, pues cada instacia de la misma corresponderá a un thread. La clase cuenta con los siguientes atributos:
La clase posee la siguiente interfaz:
class Checker: public CheckerThread {
private:
std::string result;
FileRepository& file_repo;
OutputRepository& output_repo;
void setResult(Dfs& dfs);
std::string createOutput(std::string& path);
public:
Checker(FileRepository& file_repo, OutputRepository& output_repo);
~Checker();
void verifyFile();
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta en una clase abstracta la cual encapsula el comportamiento de crear un thread para cada instancia de la clase Checker y ejecutar el correspondiente verificador. La clase cuenta con un único atributo, llamado thread correspondiente al thread que se lanzará cada vez que se llame al método start.
La clase cuenta con la siguiente interfaz:
class CheckerThread {
private:
std::thread thread;
public:
CheckerThread();
virtual ~CheckerThread();
void start();
void join();
virtual void verifyFile() = 0;
CheckerThread(const CheckerThread& other) = delete;
CheckerThread& operator=(const CheckerThread& other) = delete;
CheckerThread(CheckerThread&& other);
CheckerThread& operator=(CheckerThread&& other);
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta clase es la encargada de crear el grafo a partir del archivo que se debe verificar, para hacer esto se vale de otras dos clases FileParser y LineMap las cuales se detallaran más adelante. La clase cuenta con un único atributo llamado parser, correspondiente al LineParser encargado de convertir las lineas del archivo en información de fácil manipulación para la creación del grafo.
La clase posee la siguiente interfaz:
class GraphGenerator {
private:
FileParser parser;
int addNodes(Graph& new_graph, std::map<Node, std::set<std::string>>& edges);
int addEdges(Graph& new_graph, std::map<Node, std::set<std::string>>& edges);
public:
explicit GraphGenerator(const std::string path);
~GraphGenerator();
int generateGraph(Graph& new_graph);
};
A continuación se detallan alguno de los métodos implementados por la clase:
Es la clase encargada de abrir un archivo, leer sus lineas y parsear el texto del archivo para convertirlo en información utilizable para que así la clase GraphGenerator pueda crear un grafo a partir de ello. La clase cuenta con un único atributo, llamado file correspondiente al filestream que se desea parsear.
La clase provee la siguiente interfaz:
class FileParser {
private:
std::fstream file;
public:
explicit FileParser(const std::string file_name);
~FileParser();
LineMap parseLine();
bool hasLine() const;
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Esta es la clase encargada de almacenar de manera organizada la información leída en las líneas de los archivos, correspondiente a las instrucciones del programa bpf. La clase cuenta con los siguientes atributos:
Esta clase posee la siguiente interfaz:
class LineMap {
private:
std::map<std::string, std::string> line_map;
int status;
bool isARet() const;
bool isATag(std::string tag) const;
bool isAJump() const;
public:
LineMap();
~LineMap();
std::string getTagId();
std::set<std::string> getNeighbors();
int add(std::string key, std::string value);
bool isEmpty() const;
bool canGoToTheNexLine() const;
void invalidate();
bool isValid() const;
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Clase que representa el grafo en el cual se van almacendando los nodos correspondientes a las instrucciones del programa bpf que se debe verificar. La clase cuenta con único atributo, llamado nodes, que es una lista con los nodos pertenecientes al grafo.
Esta clase posee la siguiente interfaz:
class Graph {
private:
std::list<Node> nodes;
public:
Graph();
~Graph();
size_t getSize() const;
std::list<Node> getNodes() const;
int addNode(Node& node);
Node& getNode(const std::string id);
bool containsNode(const std::string id);
Node& getFirst();
};
A continuación se detalla un poco acerca de los métodos que posee la clase:
Clase encargada de realizar un recorrido DFS sobre un grafo permitiendo así detectar la existencia de ciclos o la existencia de instrucciones que nunca se ejecutaran. La clase cuenta con los siguientes atributos:
La clase posee la siguiente interfaz:
class Dfs {
private:
bool cycle;
bool unusedInstructions;
int visit(Graph& graph, Node& actual, std::set<Node>& visited);
public:
Dfs();
~Dfs();
int walkGraph(Graph& graph);
bool hasCycle() const;
bool hasUnusedInstructions() const;
};
A continuación se detalla un poco acerca de los métodos que posee la clase: