Add btrecord/btreplay capability
[blktrace.git] / btreplay / btrecord.c
1 /*
2  * Blktrace record utility - Convert binary trace data into bunches of IOs
3  *
4  * Copyright (C) 2007 Alan D. Brunelle <Alan.Brunelle@hp.com>
5  *
6  *  This program is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation; either version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 static char build_date[] = __DATE__ " at "__TIME__;
22
23 #include <assert.h>
24 #include <fcntl.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <unistd.h>
29 #include <sys/param.h>
30 #include <sys/stat.h>
31 #include <sys/types.h>
32 #include <dirent.h>
33
34 #if !defined(_GNU_SOURCE)
35 #       define _GNU_SOURCE
36 #endif
37 #include <getopt.h>
38
39 #include "list.h"
40 #include "btrecord.h"
41 #include "blktrace.h"
42
43 /*
44  * Per input file information
45  *
46  * @head:       Used to link up on input_files
47  * @devnm:      Device name portion of this input file
48  * @file_name:  Fully qualified name for this input file
49  * @cpu:        CPU that this file was collected on
50  * @ifd:        Input file descriptor (when opened)
51  * @tpkts:      Total number of packets processed.
52  */
53 struct ifile_info {
54         struct list_head head;
55         char *devnm, *file_name;
56         int cpu, ifd;
57         __u64 tpkts, genesis;
58 };
59
60 /*
61  * Per IO trace information
62  *
63  * @time:       Time stamp when trace was emitted
64  * @sector:     IO sector identifier
65  * @bytes:      Number of bytes transferred
66  * @rw:         Read (1) or write (0) 
67  */
68 struct io_spec {
69         __u64 time;
70         __u64 sector;
71         __u32 bytes;
72         int rw;
73 };
74
75 /*
76  * Per output file information
77  *
78  * @ofp:        Output file 
79  * @vfp:        Verbose output file
80  * @file_name:  Fully qualified name for this file
81  * @vfn:        Fully qualified name for this file
82  * @cur:        Current IO bunch being collected
83  * @iip:        Input file this is associated with
84  * @start_time: Start time of th ecurrent bunch
85  * @last_time:  Time of last packet put in
86  * @bunches:    Number of bunches processed
87  * @pkts:       Number of packets stored in bunches
88  */
89 struct io_stream {
90         FILE *ofp, *vfp;
91         char *file_name, *vfn;
92         struct io_bunch *cur;
93         struct ifile_info *iip;
94         __u64 start_time, last_time, bunches, pkts;
95 };
96
97 int data_is_native;                             // Indicates whether to swap
98 static LIST_HEAD(input_files);                  // List of all input files
99 static char *idir = ".";                        // Input directory base
100 static char *odir = ".";                        // Output directory base
101 static char *obase = "replay";                  // Output file base
102 static __u64 max_bunch_tm = (10 * 1000 * 1000); // 10 milliseconds
103 static __u64 max_pkts_per_bunch = 8;            // Default # of pkts per bunch
104 static int verbose = 0;                         // Boolean: output stats
105 static int find_traces = 0;                     // Boolean: Find traces in dir
106
107 static char usage_str[] =                                                  \
108         "\n"                                                               \
109         "\t[ -d <dir>  : --input-directory=<dir> ] Default: .\n"           \
110         "\t[ -D <dir>  : --output-directory=<dir>] Default: .\n"           \
111         "\t[ -F        : --find-traces           ] Default: Off\n"         \
112         "\t[ -h        : --help                  ] Default: Off\n"         \
113         "\t[ -m <nsec> : --max-bunch-time=<nsec> ] Default: 10 msec\n"     \
114         "\t[ -M <pkts> : --max-pkts=<pkts>       ] Default: 8\n"           \
115         "\t[ -o <base> : --output-base=<base>    ] Default: replay\n"      \
116         "\t[ -v        : --verbose               ] Default: Off\n"         \
117         "\t[ -V        : --version               ] Default: Off\n"         \
118         "\t<dev>...                                Default: None\n"        \
119         "\n";
120
121 #define S_OPTS  "d:D:Fhm:M:o:vV"
122 static struct option l_opts[] = {
123         {
124                 .name = "input-directory",
125                 .has_arg = required_argument,
126                 .flag = NULL,
127                 .val = 'd'
128         },
129         {
130                 .name = "output-directory",
131                 .has_arg = required_argument,
132                 .flag = NULL,
133                 .val = 'D'
134         },
135         {
136                 .name = "find-traces",
137                 .has_arg = no_argument,
138                 .flag = NULL,
139                 .val = 'F'
140         },
141         {
142                 .name = "help",
143                 .has_arg = no_argument,
144                 .flag = NULL,
145                 .val = 'h'
146         },
147         {
148                 .name = "max-bunch-time",
149                 .has_arg = required_argument,
150                 .flag = NULL,
151                 .val = 'm'
152         },
153         {
154                 .name = "max_pkts",
155                 .has_arg = required_argument,
156                 .flag = NULL,
157                 .val = 'M'
158         },
159         {
160                 .name = "output-base",
161                 .has_arg = required_argument,
162                 .flag = NULL,
163                 .val = 'o'
164         },
165         {
166                 .name = "verbose",
167                 .has_arg = no_argument,
168                 .flag = NULL,
169                 .val = 'v'
170         },
171         {
172                 .name = "version",
173                 .has_arg = no_argument,
174                 .flag = NULL,
175                 .val = 'V'
176         },
177         {
178                 .name = NULL
179         }
180 };
181
182 #define ERR_ARGS                        1
183 #define ERR_SYSCALL                     2
184 #define fatal(errstring, exitval, arg...)                               \
185         do {                                                            \
186                 if (errstring) perror(errstring);                       \
187                 fprintf(stderr, ##arg);                                 \
188                 exit(exitval);                                          \
189                 /*NOTREACHED*/                                          \
190         } while (0)
191
192 /**
193  * match - Return true if this trace is a proper QUEUE transaction
194  * @action: Action field from trace
195  */
196 static inline int match(__u32 action)
197 {
198         return ((action & 0xffff) == __BLK_TA_QUEUE) &&
199                                        (action & BLK_TC_ACT(BLK_TC_QUEUE));
200 }
201
202 /**
203  * usage - Display usage string and version
204  */
205 static void usage(void)
206 {
207         fprintf(stderr, "Usage: btrecord -- version %s\n%s", 
208                 my_btversion, usage_str);
209 }
210
211 /**
212  * write_file_hdr - Seek to and write btrecord file header
213  * @stream: Output file information
214  * @hdr: Header to write
215  */
216 static void write_file_hdr(struct io_stream *stream, struct io_file_hdr *hdr)
217 {
218         hdr->version = mk_btversion(btver_mjr, btver_mnr, btver_sub);
219
220         if (verbose) {
221                 fprintf(stderr, "\t%s: %llx %llx %llx %llx\n", 
222                         stream->file_name,
223                         (long long unsigned)hdr->version,
224                         (long long unsigned)hdr->genesis,
225                         (long long unsigned)hdr->nbunches,
226                         (long long unsigned)hdr->total_pkts);
227         }
228
229         fseek(stream->ofp, 0, SEEK_SET);
230         if (fwrite(hdr, sizeof(*hdr), 1, stream->ofp) != 1) {
231                 fatal(stream->file_name, ERR_SYSCALL, "Hdr write failed\n");
232                 /*NOTREACHED*/
233         }
234 }
235
236 /**
237  * io_bunch_create - Allocate & initialize an io_bunch
238  * @io_stream: IO stream being added to
239  * @pre_stall: Amount of time that this bunch should be delayed by
240  * @start_time: Records current start 
241  */
242 static inline void io_bunch_create(struct io_stream *stream, __u64 start_time)
243 {
244         struct io_bunch *cur = malloc(sizeof(*cur));
245
246         memset(cur, 0, sizeof(*cur));
247
248         cur->hdr.npkts = 0;
249         cur->hdr.time_stamp = stream->start_time = start_time;
250
251         stream->cur = cur;
252 }
253
254 /**
255  * io_bunch_add - Add an IO to the current bunch of IOs
256  * @stream: Per-output file stream information
257  * @spec: IO trace specification
258  *
259  * Returns update bunch information
260  */
261 static void io_bunch_add(struct io_stream *stream, struct io_spec *spec)
262 {
263         struct io_bunch *cur = stream->cur;
264         struct io_pkt iop = {
265                 .sector = spec->sector,
266                 .nbytes = spec->bytes,
267                 .rw = spec->rw
268         };
269
270         assert(cur != NULL);
271         assert(cur->hdr.npkts < BT_MAX_PKTS);
272         assert(stream->last_time == 0 || stream->last_time <= spec->time);
273
274         cur->pkts[cur->hdr.npkts++] = iop;      // Struct copy
275         stream->last_time = spec->time;
276 }
277
278 /**
279  * rem_input_file - Release resources associated with an input file
280  * @iip: Per-input file information
281  */
282 static void rem_input_file(struct ifile_info *iip)
283 {
284         list_del(&iip->head);
285
286         close(iip->ifd);
287         free(iip->file_name);
288         free(iip->devnm);
289         free(iip);
290 }
291
292 /**
293  * __add_input_file - Allocate and initialize per-input file structure
294  * @cpu: CPU for this file
295  * @devnm: Device name for this file
296  * @file_name: Fully qualifed input file name
297  */
298 static void __add_input_file(int cpu, char *devnm, char *file_name)
299 {
300         struct ifile_info *iip = malloc(sizeof(*iip));
301
302         iip->cpu = cpu;
303         iip->tpkts = 0;
304         iip->genesis = 0;
305         iip->devnm = strdup(devnm);
306         iip->file_name = strdup(file_name);
307         iip->ifd = open(file_name, O_RDONLY);
308         if (iip->ifd < 0) {
309                 fatal(file_name, ERR_ARGS, "Unable to open\n");
310                 /*NOTREACHED*/
311         }
312
313         list_add_tail(&iip->head, &input_files);
314 }
315
316 /**
317  * add_input_file - Set up the input file name
318  * @devnm: Device name to use
319  */
320 static void add_input_file(char *devnm)
321 {
322         struct list_head *p;
323         int cpu, found = 0;
324
325         __list_for_each(p, &input_files) {
326                 struct ifile_info *iip = list_entry(p, struct ifile_info, head);
327                 if (strcmp(iip->devnm, devnm) == 0)
328                         return;
329         }
330
331         for (cpu = 0; ; cpu++) {
332                 char full_name[MAXPATHLEN];
333
334                 sprintf(full_name, "%s/%s.blktrace.%d", idir, devnm, cpu);
335                 if (access(full_name, R_OK) != 0)
336                         break;
337
338                 __add_input_file(cpu, devnm, full_name);
339                 found++;
340         }
341
342         if (!found) {
343                 fatal(NULL, ERR_ARGS, "No traces found for %s\n", devnm);
344                 /*NOTREACHED*/
345         }
346 }
347
348 static void find_input_files(char *idir)
349 {
350         struct dirent *ent;
351         DIR *dir = opendir(idir);
352
353         if (dir == NULL) {
354                 fatal(idir, ERR_ARGS, "Unable to open %s\n", idir);
355                 /*NOTREACHED*/
356         }
357
358         while ((ent = readdir(dir)) != NULL) {
359                 char *p, *dsf = malloc(256);
360
361                 if (strstr(ent->d_name, ".blktrace.") == NULL)
362                         continue;
363
364                 dsf = strdup(ent->d_name);
365                 p = index(dsf, '.');
366                 assert(p != NULL);
367                 *p = '\0';
368                 add_input_file(dsf);
369                 free(dsf);
370         }
371
372         closedir(dir);
373 }
374
375 /**
376  * handle_args - Parse passed in argument list
377  * @argc: Number of arguments in argv
378  * @argv: Arguments passed in
379  *
380  * Does rudimentary parameter verification as well.
381  */
382 void handle_args(int argc, char *argv[])
383 {
384         int c;
385
386         while ((c = getopt_long(argc, argv, S_OPTS, l_opts, NULL)) != -1) {
387                 switch (c) {
388                 case 'd':
389                         idir = optarg;
390                         if (access(idir, R_OK | X_OK) != 0) {
391                                 fatal(idir, ERR_ARGS, 
392                                       "Invalid input directory specified\n");
393                                 /*NOTREACHED*/
394                         }
395                         break;
396
397                 case 'D':
398                         odir = optarg;
399                         if (access(odir, R_OK | X_OK) != 0) {
400                                 fatal(odir, ERR_ARGS, 
401                                       "Invalid output directory specified\n");
402                                 /*NOTREACHED*/
403                         }
404                         break;
405
406                 case 'F': 
407                         find_traces = 1;
408                         break;
409
410                 case 'h': 
411                         usage(); 
412                         exit(0);
413                         /*NOTREACHED*/
414
415                 case 'm':
416                         max_bunch_tm = (__u64)atoll(optarg);
417                         if (max_bunch_tm < 1) {
418                                 fprintf(stderr, "Invalid bunch time %llu\n",
419                                         (unsigned long long)max_bunch_tm);
420                                 exit(ERR_ARGS);
421                                 /*NOTREACHED*/
422                         }
423                         break;
424
425                 case 'M':
426                         max_pkts_per_bunch = (__u64)atoll(optarg);
427                         if (!((1 <= max_pkts_per_bunch) && 
428                                                 (max_pkts_per_bunch < 513))) {
429                                 fprintf(stderr, "Invalid max pkts %llu\n",
430                                         (unsigned long long)max_pkts_per_bunch);
431                                 exit(ERR_ARGS);
432                                 /*NOTREACHED*/
433                         }
434                         break;
435
436                 case 'o':
437                         obase = optarg;
438                         break;
439
440                 case 'V':
441                         fprintf(stderr, "btrecord -- version %s\n", 
442                                 my_btversion);
443                         fprintf(stderr, "            Built on %s\n", build_date);
444                         exit(0);
445                         /*NOTREACHED*/
446
447                 case 'v':
448                         verbose++;
449                         break;
450
451                 default:
452                         usage();
453                         fatal(NULL, ERR_ARGS, "Invalid command line\n");
454                         /*NOTREACHED*/
455                 }
456         }
457
458         while (optind < argc)
459                 add_input_file(argv[optind++]);
460
461         if (find_traces)
462                 find_input_files(idir);
463
464         if (list_len(&input_files) == 0) {
465                 fatal(NULL, ERR_ARGS, "Missing required input file name(s)\n");
466                 /*NOTREACHED*/
467         }
468 }
469
470 /**
471  * next_io - Retrieve next Q trace from input stream
472  * @iip: Per-input file information
473  * @spec: IO specifier for trace
474  *
475  * Returns 0 on end of file, 1 if valid data returned.
476  */
477 static int next_io(struct ifile_info *iip, struct io_spec *spec)
478 {
479         ssize_t ret;
480         __u32 action;
481         __u16 pdu_len;
482         struct blk_io_trace t;
483
484 again:
485         ret = read(iip->ifd, &t, sizeof(t));
486         if (ret < 0) {
487                 fatal(iip->file_name, ERR_SYSCALL, "Read failed\n");
488                 /*NOTREACHED*/
489         }
490         else if (ret == 0)
491                 return 0;
492         else if (ret < (ssize_t)sizeof(t)) {
493                 fprintf(stderr, "WARNING: Short read on %s (%d)\n", 
494                         iip->file_name, (int)ret);
495                 return 0;
496         }
497
498         if (data_is_native == -1)
499                 check_data_endianness(t.magic);
500
501         assert(data_is_native >= 0);
502         if (data_is_native) {
503                 spec->time = t.time;
504                 spec->sector = t.sector;
505                 spec->bytes = t.bytes;
506                 action = t.action;
507                 pdu_len = t.pdu_len;
508         }
509         else {
510                 spec->time = be64_to_cpu(t.time);
511                 spec->sector = be64_to_cpu(t.sector);
512                 spec->bytes = be32_to_cpu(t.bytes);
513                 action = be32_to_cpu(t.action);
514                 pdu_len = be16_to_cpu(t.pdu_len);
515         }
516
517
518         if (pdu_len) {
519                 char buf[pdu_len];
520
521                 ret = read(iip->ifd, buf, pdu_len);
522                 if (ret < 0) {
523                         fatal(iip->file_name, ERR_SYSCALL, "Read PDU failed\n");
524                         /*NOTREACHED*/
525                 }
526                 else if (ret < (ssize_t)pdu_len) {
527                         fprintf(stderr, "WARNING: Short PDU read on %s (%d)\n", 
528                                 iip->file_name, (int)ret);
529                         return 0;
530                 }
531         }
532
533         iip->tpkts++;
534         if (!match(action))
535                 goto again;
536
537         spec->rw = (action & BLK_TC_ACT(BLK_TC_READ)) ? 1 : 0;
538         if (verbose > 1)
539                 fprintf(stderr, "%2d: %10llu+%10llu (%d) @ %10llx\n",
540                         iip->cpu, (long long unsigned)spec->sector,
541                         (long long unsigned)spec->bytes / 512LLU,
542                         spec->rw, (long long unsigned)spec->time);
543
544         if (iip->genesis == 0) {
545                 iip->genesis = spec->time;
546                 if (verbose > 1)
547                         fprintf(stderr, "\tSetting new genesis: %llx(%d)\n",
548                                 (long long unsigned)iip->genesis, iip->cpu);
549         }
550         else if (iip->genesis > spec->time)
551                 fatal(NULL, ERR_SYSCALL, 
552                         "Time inversion? %llu ... %llu\n",
553                         (long long unsigned )iip->genesis, 
554                         (long long unsigned )spec->time);
555
556         return 1;
557 }
558
559 /**
560  * bunch_output_hdr - Output bunch header
561  */
562 static inline void bunch_output_hdr(struct io_stream *stream)
563 {
564         struct io_bunch_hdr *hdrp = &stream->cur->hdr;
565
566         assert(0 < hdrp->npkts && hdrp->npkts <= BT_MAX_PKTS);
567         if (fwrite(hdrp, sizeof(struct io_bunch_hdr), 1, stream->ofp) != 1) {
568                 fatal(stream->file_name, ERR_SYSCALL, "fwrite(hdr) failed\n");
569                 /*NOTREACHED*/
570         }
571
572         if (verbose) {
573                 __u64 off = hdrp->time_stamp - stream->iip->genesis;
574
575                 assert(stream->vfp);
576                 fprintf(stream->vfp, "------------------\n");
577                 fprintf(stream->vfp, "%4llu.%09llu %3llu\n", 
578                         (unsigned long long)off / (1000 * 1000 * 1000),
579                         (unsigned long long)off % (1000 * 1000 * 1000), 
580                         (unsigned long long)hdrp->npkts);
581                 fprintf(stream->vfp, "------------------\n");
582         }
583 }
584
585 /**
586  * bunch_output_pkt - Output IO packets
587  */
588 static inline void bunch_output_pkts(struct io_stream *stream)
589 {
590         struct io_pkt *p = stream->cur->pkts;
591         size_t npkts = stream->cur->hdr.npkts;
592
593         assert(0 < npkts && npkts <= BT_MAX_PKTS);
594         if (fwrite(p, sizeof(struct io_pkt), npkts, stream->ofp) != npkts) {
595                 fatal(stream->file_name, ERR_SYSCALL, "fwrite(pkts) failed\n");
596                 /*NOTREACHED*/
597         }
598
599         if (verbose) {
600                 size_t i;
601
602                 assert(stream->vfp);
603                 for (i = 0; i < npkts; i++, p++)
604                         fprintf(stream->vfp, "\t%1d %10llu\t%10llu\n",
605                                 p->rw, 
606                                 (unsigned long long)p->sector,
607                                 (unsigned long long)p->nbytes / 512);
608         }
609 }
610
611 /**
612  * stream_flush - Flush current bunch of IOs out to the output stream
613  * @stream: Per-output file stream information
614  */
615 static void stream_flush(struct io_stream *stream)
616 {
617         struct io_bunch *cur = stream->cur;
618
619         if (cur) {
620                 if (cur->hdr.npkts) {
621                         assert(cur->hdr.npkts <= BT_MAX_PKTS);
622                         bunch_output_hdr(stream);
623                         bunch_output_pkts(stream);
624
625                         stream->bunches++;
626                         stream->pkts += cur->hdr.npkts;
627                 }
628                 free(cur);
629         }
630 }
631
632 /**
633  * bunch_done - Returns true if current bunch is either full, or next IO is late
634  * @stream: Output stream information
635  * @spec: IO trace specification
636  */
637 static inline int bunch_done(struct io_stream *stream, struct io_spec *spec)
638 {
639         if (stream->cur->hdr.npkts >= max_pkts_per_bunch)
640                 return 1;
641
642         if ((spec->time - stream->start_time) > max_bunch_tm)
643                 return 1;
644
645         return 0;
646 }
647
648 /**
649  * stream_add_io - Add an IO trace to the current stream
650  * @stream: Output stream information
651  * @spec: IO trace specification
652  */
653 static void stream_add_io(struct io_stream *stream, struct io_spec *spec)
654 {
655
656         if (stream->cur == NULL)
657                 io_bunch_create(stream, spec->time);
658         else if (bunch_done(stream, spec)) {
659                 stream_flush(stream);
660                 io_bunch_create(stream, spec->time);
661         }
662
663         io_bunch_add(stream, spec);
664 }
665
666 /**
667  * stream_open - Open output stream for specified input stream
668  * @iip: Per-input file information
669  */
670 static struct io_stream *stream_open(struct ifile_info *iip)
671 {
672         char ofile_name[MAXPATHLEN];
673         struct io_stream *stream = malloc(sizeof(*stream));
674         struct io_file_hdr io_file_hdr = {
675                 .genesis = 0,
676                 .nbunches = 0,
677                 .total_pkts = 0
678         };
679
680         memset(stream, 0, sizeof(*stream));
681
682         sprintf(ofile_name, "%s/%s.%s.%d", odir, iip->devnm, obase, iip->cpu);
683         stream->ofp = fopen(ofile_name, "w");
684         if (!stream->ofp) {
685                 fatal(ofile_name, ERR_SYSCALL, "Open failed\n");
686                 /*NOTREACHED*/
687         }
688
689         stream->iip = iip;
690         stream->cur = NULL;
691         stream->bunches = stream->pkts = 0;
692         stream->last_time = 0;
693         stream->file_name = strdup(ofile_name);
694
695         write_file_hdr(stream, &io_file_hdr);
696
697         if (verbose) {
698                 char vfile_name[MAXPATHLEN];
699
700                 sprintf(vfile_name, "%s/%s.%s.%d.rec", odir, iip->devnm,
701                         obase, iip->cpu);
702                 stream->vfp = fopen(vfile_name, "w");
703                 if (!stream->vfp) {
704                         fatal(vfile_name, ERR_SYSCALL, "Open failed\n");
705                         /*NOTREACHED*/
706                 }
707
708                 stream->vfn = strdup(vfile_name);
709         }
710
711         data_is_native = -1;
712         return stream;
713 }
714
715 /**
716  * stream_close - Release resources associated with an output stream
717  * @stream: Stream to release
718  */
719 static void stream_close(struct io_stream *stream)
720 {
721         struct io_file_hdr io_file_hdr = {
722                 .genesis = stream->iip->genesis,
723                 .nbunches = stream->bunches,
724                 .total_pkts = stream->pkts
725         };
726
727         stream_flush(stream);
728         write_file_hdr(stream, &io_file_hdr);
729         fclose(stream->ofp);
730
731         if (verbose && stream->bunches) {
732                 fprintf(stderr, 
733                         "%s:%d: %llu pkts (tot), %llu pkts (replay), "
734                                         "%llu bunches, %.1lf pkts/bunch\n",
735                         stream->iip->devnm, stream->iip->cpu,
736                         (unsigned long long)stream->iip->tpkts, 
737                         (unsigned long long)stream->pkts, 
738                         (unsigned long long)stream->bunches,
739                         (double)(stream->pkts) / (double)(stream->bunches));
740
741                 fclose(stream->vfp);
742                 free(stream->vfn);
743         }
744
745         free(stream->file_name);
746         free(stream);
747 }
748
749 /**
750  * process - Process one input file to an output file
751  * @iip: Per-input file information
752  */
753 static void process(struct ifile_info *iip)
754 {
755         struct io_spec spec;
756         struct io_stream *stream;
757
758         stream = stream_open(iip);
759         while (next_io(iip, &spec))
760                 stream_add_io(stream, &spec);
761         stream_close(stream);
762
763         rem_input_file(iip);
764 }
765
766 /**
767  * main - 
768  * @argc: Number of arguments
769  * @argv: Array of arguments
770  */
771 int main(int argc, char *argv[])
772 {
773         struct list_head *p, *q;
774
775         handle_args(argc, argv);
776         list_for_each_safe(p, q, &input_files)
777                 process(list_entry(p, struct ifile_info, head));
778
779         return 0;
780 }