Mis à jour le 7 mars 2006
TD 2 : Entrées-sorties

TD 2 : Entrées-sorties

17 janvier 2006

1  Question de cours

On considère une application mini-base de données qui conserve des données persistantes dans un fichier data. On suppose que deux applications ne peuvent pas tourner en parallèle, mais seulement l'une après l'autre. Une session lit les données en mémoire, en modifie une partie puis réécrit le résultat dans le fichier data.

Quel est le problème essentiel auquel il faut penser?

(corrigé)
La cohérence des données en cas de panne.

On voudrait qu'en cas de panne les données soit toujours correctes, ie. le fichier data contiennent les nouvelles données correctement écrites ou les anciennes données. Comment réaliser cela?
(corrigé)
Comme l'écriture n'est pas atomique, il n'y a pas d'autre solution que d'écrire les données dans un fichier temporaire puis de le renommer avec le nom du fichier définitif. La solution est nécessairement imparfaite. Expliquez pourquoi.
(corrigé)
Les éventuels liens « durs » vers le fichier d'origine ne sont pas mis à jour, ils gardent l'ancien contenu du fichier car leur inode n'a pas changé alors que l'inode du fichier définitif est maintenant celui du fichier temporaire.

Que manque-t-il pour s'en sortir?
(corrigé)
Une primitive qui permettrait de s'en sortir serait l'échange atomique du contenu de deux fichiers avec préservation de l'inode et des méta informations. Cette opération est réaliste alors que l'écriture de fichiers de façon atomique ne l'est pas.

2  Copie de fichier

On désire écrire une commande mon_cat équivalente à la commande cat du système. Celle-ci copie sur sa sortie standard le contenu des fichiers dont les noms lui sont donnés en argument.

Écrire une fonction à deux arguments copy_data:Unix.file_descr -> Unix.file_descr -> unit, qui prend deux descripteurs de fichier en argument et copie tout ce qui peut être lu sur le premier dans le second, en utilisant les fonctions Unix.read et Unix.write.
(corrigé)
      
    let buffer_size = 4096
    let buffer = String.create buffer_size

    let rec copy_data fd_in fd_out =
        match Unix.read fd_in buffer 0 buffer_size with
          0 -> ()
        | r ->
            ignore (Unix.write fd_out buffer 0 r);
            copy_data fd_in fd_out;;
Écrire la commande mon_cat, en utilisant la fonction précédente sur chacun de ses arguments et en copiant dans la sortie standard (Unix.stdout). Pour simplifier, on considèrera toute erreur de lecture/écriture comme fatale.
(corrigé)
      
open Sys;;

let mon_cat () =

  for i = 1 to Array.length argv - 1 do
    (* Les erreurs sont fatales dans cet exemple... *)
    let filename = Sys.argv.(iin
    let fd_in = Unix.openfile filename [Unix.O_RDONLY] 0o400 in
    copy_data fd_in Unix.stdout;
    Unix.close fd_in
  done;;

Unix.handle_unix_error mon_cat ();;

3  Entrée-sortie standard

On désire écrire une commande mon_tr qui remplace certains caractères reçus sur l'entrée standard avant de les retourner sur la sortie standard et qui a le même comportement que la commande tr du système (sans options). Par exemple, mon_tr abcd ijk remplace les caractères a, b et c par, respectivement, i, j et k et le caractère d par k.

Modifier la fonction copy_data précédente pour qu'elle prenne un argument supplémentaire, une fonction de type char -> char qu'elle applique sur chaque caractère lu avant de le copier.
(corrigé)
      
    let buffer_size = 4096
    let buffer = String.create buffer_size

    let copy_data fd_in fd_out translate =
      let rec copy_loop () =
        match Unix.read fd_in buffer 0 buffer_size with
          0 -> ()
        | r ->
            for i = 0 to r-1 do buffer.[i] <- translate buffer.[idone;
            ignore (Unix.write fd_out buffer 0 r);
            copy_loop () in
      copy_loop ()
Écrire une fonction à deux arguments tr_function : string -> string -> (char -> char) qui retourne une fonction de traduction qui traduit chaque caractère qui lui sera passé en argument selon la correspondance définie par les chaînes de caractères reçues en premier et second argument de la fonction tr_function (la longueur d'une chaîne de caractères est retournée par String.length). Pour effectuer la traduction, on utilisera une chaîne de caractères de longueur 256 (créée par la fonction String.create 256) telle que le caractère à la position i dans cette chaîne (s.[i]) corresponde à la traduction du caractère de code i (Char.code c). Inversement, le caractère de code i est obtenu par Char.chr i.
(corrigé)
      
let tr_function s1 s2 =
     let translation = String.create 256 in
     for i = 0 to 255 do translation.[i] <- Char.chr i done;
     let l1 = String.length s1 - 1 and l2 = String.length s2 - 1 in
     let l12 = min l1 l2 in
     for i = 0 to l12 do translation.[Char.code s1.[i]] <- s2.[idone;
     for i = l12+1 to l1 do translation.[Char.code s1.[i]] <- s2.[l12done;
     fun c -> translation.[Char.code c];;
Ensuite, écrire la fonction principale qui lit sur l'entrée standard (Unix.stdin) et copie sur la sortie standard les données après traduction. Pour simplifier, on considèrera toute erreur de lecture/écriture comme fatale.
(corrigé)
      
open Sys;;
open Unix;;

let usage () =
  prerr_string ("Usage: "^(Filename.basename argv.(0))^" <input_string> <output_string>");
  prerr_newline();
  exit 1;;

let buffer_size = 4096;;
let mon_tr () =
  if Array.length argv <> 3 || String.length argv.(2) = 0 then usage()
  else
    begin
      let tr_char = tr_function argv.(1) argv.(2) in
      copy_data stdin stdout tr_char
    end;;

handle_unix_error mon_tr ();;

4  Déplacement dans les fichiers

On désire écrire une commande mon_tail -n N file qui affiche les N dernières lignes du fichier régulier file. Pour cela, il faut lire le fichier en « sens inverse ».

Décrire le schéma général du programme.
(corrigé)
En supposant qu'au départ on se trouve à la fin du fichier et que le fichier se termine par un caractère de fin de ligne, à chaque étape, grâce à Unix.lseek et Unix.read, on lit les buffer_size caractères précédents; on compte le nombre de retours à la ligne qu'ils contiennent; s'il y en a au moins N+1, on affiche les N lignes correspondantes et on termine; sinon, on recommence.

Écrire une fonction really_read qui force la lecture d'exactement size caractères sur un descripteur déjà ouvert et qui les place dans une chaîne de caractères à partir d'un indice passé en argument. Si la lecture n'est pas possible, lever une exception End_of_file.
(corrigé)
      
open Unix;;
let rec really_read desc buffer start length =
  if length <= 0 then ()
  else
    match read desc buffer start length with
      0 -> raise End_of_file
    | r -> really_read desc buffer (start+r) (length-r);;
Écrire une fonction set_pos : Unix.file_descr -> int -> unit qui positionne le descripteur de fichier à la position donnée en argument et vérifie que la position d'arrivée est correcte. On utilisera la fonction Unix.lseek pour se déplacer dans le fichier.
(corrigé)
      
let set_pos desc pos =
  if pos <> lseek desc pos SEEK_SET then
    failwith "lseek: mauvaise position de retour"
Écrire une fonction get_size : Unix.file_descr -> int qui retourne la taille du fichier associé à un descripteur.
(corrigé)
      
let get_size desc = (fstat desc).st_size
Écrire une fonction find_lines, telle que find_lines buffer size nb retourne l'indice de début de la (nb-1)-ème ligne en partant de la fin (size) de la chaîne de caractères buffer. Si la chaîne de caractères contient moins de nb retours à la ligne la fonction lèvera une exception contenant le nombre de lignes qu'il reste à lire.
(corrigé)
      
exception Not_enough_lines of int

let find_lines buffer size nb =
  let rec find n index =
    if index < 0 then raise (Not_enough_lines n)
    else if buffer.[index] = '\nthen
      if n <= 1 then index + 1
      else find (n - 1) (index - 1)
    else find n (index-1) in
  find nb (size - 1);;
Écrire une fonction récursive tail, telle que tail desc size nb retourne la position absolue dans le fichier de la (nb-1)-ème ligne sur le descripteur desc à partir de la position absolue size. Attention, il faut faire attention à ne pas demander de lire plus que la taille disponible sur le descripteur sous peine que really_read lève une exception.
(corrigé)
      
let buffer_size = 4096
let buffer = String.create buffer_size

let rec tail desc size nb =
  if size > 0 then
    begin
      let real_size = min size buffer_size in
      let offset = size - real_size in
      set_pos desc offset;
      really_read desc buffer 0 real_size;
      try
        let index = find_lines buffer real_size nb in
        offset + index
      with Not_enough_lines missing ->
        tail desc offset missing
    end else 0;;
Finalement, écrire la fonction tail_lines telle que tail_lines desc nb affiche les nb dernières lignes du descripteur desc, en ne tenant pas compte du dernier caractère du fichier. On pourra aussi utiliser la fonction copy_data des exercices précédents.
(corrigé)
      
let copy_data fd_in fd_out =
  let rec copy_loop () =
    match Unix.read fd_in buffer 0 buffer_size with
      0 -> ()
    | r ->
        ignore (Unix.write fd_out buffer 0 r);
        copy_loop () in
  copy_loop ()

let tail_lines desc nb =
  let pos = tail desc (get_size desc - 1) nb in
  ignore (lseek desc pos SEEK_SET);
  copy_data desc Unix.stdout
On désire ajouter un option -f, équivalente à celle de la commande tail du système, qui permet d'afficher les modifications apportées au fichier au fur et à mesure quelles sont effectuées. Donner le principe ce cette fonction.
(corrigé)
Une fois qu'on a affiché les N dernières lignes du fichier, on se positionne à la fin du fichier, et on essaye de lire (par Unix.read) à partir de là. Si read réussit à lire quelque chose, on l'affiche aussitôt et on recommence. Si read renvoie 0, on attend un peu (Unix.sleep 1), puis on recommence. Écrire une fonction monitor qui implante l'option -f. On pourra utiliser une fonction récursive intermédiaire qui affiche toutes les données lues entre la position courante du descripteur et la fin de fichier. On supposera pour cette fonction que le fichier ne peut pas être tronqué.
(corrigé)
      
let monitor desc size =
  ignore (lseek desc 0 SEEK_END);
  let buffer = String.create buffer_size in
  while true do
    let rec read_all() =
      match read desc buffer 0 buffer_size with
       0 -> ()
     | len ->
          ignore (write stdout buffer 0 len);
          read_all () in
    read_all();
    sleep 1;
  done;;
Écrire une nouvelle fonction monitor qui prend en compte le fait que le fichier puisse être tronqué. Dans ce cas, la commande doit commencer à afficher les lignes à partir de la nouvelle taille du fichier.
(corrigé)
      
   let monitor desc size =
     let buffer = String.create buffer_size in
     let rec read_more last_pos =
       let rec read_all pos =
         match read desc buffer 0 buffer_size with
           0 when pos > last_pos -> pos
         | 0 ->
             let pos = lseek desc 0 SEEK_END in
             if pos < last_pos then prerr_endline "Truncated";
             pos
         | len ->
             ignore (write stdout buffer 0 len);
             read_all (pos + lenin
       let new_pos = read_all last_pos in
       sleep 1;
       read_more new_pos in
     read_more size;;
Terminer le programme. Attention, le fichier peut ne pas se terminer par un retour à la ligne.
(corrigé)
Pour éviter le problème du dernier caractère en fin de fichier, on le retire pour la recherche des lignes puis on l'affiche quoi qu'il arrive.
      
open Sys;;
open Arg;;

let lines = ref 4;;
let follow = ref false;;

let usage () =
  prerr_string ("Usage: "^argv.(0)^" [options] <file>");
  prerr_newline ();
  exit 1;;

let process_file filename =
  let desc = openfile filename [ O_RDONLY ] 0 in
  tail_lines desc !lines;
  if !follow then monitor desc (get_size desc)
  else close desc;;

let mon_tail () =
  let options =
    [ ("-n"Int (fun k -> lines := k)," line number");
      ("-f"Set follow"follow appended") ] in
  parse options process_file "file name";;

Printexc.print (handle_unix_error mon_tail) ();;

5  Lectures-écritures dans des fichiers

Écrire une commande mon_split n file qui découpe le fichier file en plusieurs fichiers (de noms file-0, file-1, etc.) de taille n (sauf éventuellement le dernier) et qui a un comportement équivalent à la commande split du système.

Pour réaliser cette commande, on suppose que la taille n peut être très grande et donc qu'il n'est pas envisageable d'allouer un tampon de cette taille pour lire puis écrire en deux opérations. On utilisera donc un tampon de taille fixe pour lire les données. Il faut alors faire attention au cas ou la taille des fichiers est inférieure à la taille des données lues.

On pourra en particulier modifier la fonction copy_data des exercices précédents en lui ajoutant en paramètre la quantité de caractères à copier, et en retournant le nombre de caractères non-copiés (quand la fin du fichier est atteinte).
(corrigé)
      
open Unix;;

(* program constants *)
let slice_size = ref 1024;;
let buffer_size = 4096 ;;
let buffer = String.create buffer_size

(* name generation function for output files *)
let slicename name index = Printf.sprintf "%s-%d" name index;;

let rec copy_data fd_in fd_out tocopy =
  if tocopy > 0 then
    let len = Unix.read fd_in buffer 0 (min tocopy buffer_sizein
    if len > 0 then begin
        ignore (Unix.write fd_out buffer 0 len);
        copy_data fd_in fd_out (tocopy - len)
      end else
      tocopy
  else
    0

(* do the job *)
let split file size =
  Printf.fprintf Pervasives.stderr "file=%s size=%d" file size;
  prerr_newline();

  let fd_in = Unix.openfile file [ O_RDONLY ] 0 in

  let rec copy_slices slice =
    let fd_out = Unix.openfile (slicename file slice)
      [O_WRONLY;O_CREAT;O_TRUNC] 0o666 in
    let left = copy_data fd_in fd_out size in
    Unix.close fd_out;
    if left = 0 then copy_slices (slice + 1)
  in
  copy_slices 0;
  Unix.close fd_in

(* errors and arguments handling functions*)
let errors = ref false;;
let process_file file =
  try split file !slice_size
  with Unix_error (e,b,c) ->
    errors := true;
    prerr_string (Sys.argv.(0)^": " ^c": " ^(error_message e));
    prerr_newline ();;

let set_size nb =
  if nb > 0 then slice_size := nb
  else raise (Arg.Bad "Size must be strictly positive");;
let usage = "Usage: " ^ Sys.argv.(0) ^ " [ option ] filename";;

(* main function *)
let mon_split () =
  Arg.parse [ ("-b"Arg.Int set_size"byte number") ] process_file usage;
  if !errors then exit 1;;

handle_unix_error mon_split();;

6  Exercice

Écrire une fonction mon_write qui se comporte comme write, sans utiliser celle-ci! —on utilisera seuleument single_write.
(corrigé)
      
open Unix
let mon_write desc buf offset len =
  let rec write offset left =
    if len > 0 then
      let n = single_write desc buf offset left in
      write (offset + n) (left - n)
    else len in
  write offset len;;
Rappeler un des problèmes de single_write? Comment pourriez-vous corriger celui-ci?
(corrigé)
Lorsqu'une erreur se produit alors qu'une partie a déjà été écrite, on perd cette information. Pour y remédier, on peut utiliser une exception Partial_write et retourner à la fois l'erreur et le nombre exact d'octets écrits (lorsque celui-ci est non nul, sinon on reporte l'erreur comme d'habitude).
      
exception Partial_write of exn * int
let mon_write desc buf offset len =
  let rec write offset left =
    if left > 0 then
      let n =
        begin try single_write desc buf offset left with
        | Unix_error (___as exn when left < len ->
            raise (Partial_write (exnlen -left))
        end in
      write (offset + n) (left -n)
    else len in
  write offset len;;



Ce document a été traduit de LATEX par HEVEA