| /* |
| * Blktrace record utility - Convert binary trace data into bunches of IOs |
| * |
| * Copyright (C) 2007 Alan D. Brunelle <Alan.Brunelle@hp.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| */ |
| |
| static char build_date[] = __DATE__ " at "__TIME__; |
| |
| #include <assert.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <sys/param.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <dirent.h> |
| #include <stdarg.h> |
| |
| #if !defined(_GNU_SOURCE) |
| # define _GNU_SOURCE |
| #endif |
| #include <getopt.h> |
| |
| #include "list.h" |
| #include "btrecord.h" |
| #include "blktrace.h" |
| |
| /* |
| * Per input file information |
| * |
| * @head: Used to link up on input_files |
| * @devnm: Device name portion of this input file |
| * @file_name: Fully qualified name for this input file |
| * @cpu: CPU that this file was collected on |
| * @ifd: Input file descriptor (when opened) |
| * @tpkts: Total number of packets processed. |
| */ |
| struct ifile_info { |
| struct list_head head; |
| char *devnm, *file_name; |
| int cpu, ifd; |
| __u64 tpkts, genesis; |
| }; |
| |
| /* |
| * Per IO trace information |
| * |
| * @time: Time stamp when trace was emitted |
| * @sector: IO sector identifier |
| * @bytes: Number of bytes transferred |
| * @rw: Read (1) or write (0) |
| */ |
| struct io_spec { |
| __u64 time; |
| __u64 sector; |
| __u32 bytes; |
| int rw; |
| }; |
| |
| /* |
| * Per output file information |
| * |
| * @ofp: Output file |
| * @vfp: Verbose output file |
| * @file_name: Fully qualified name for this file |
| * @vfn: Fully qualified name for this file |
| * @cur: Current IO bunch being collected |
| * @iip: Input file this is associated with |
| * @start_time: Start time of th ecurrent bunch |
| * @last_time: Time of last packet put in |
| * @bunches: Number of bunches processed |
| * @pkts: Number of packets stored in bunches |
| */ |
| struct io_stream { |
| FILE *ofp, *vfp; |
| char *file_name, *vfn; |
| struct io_bunch *cur; |
| struct ifile_info *iip; |
| __u64 start_time, last_time, bunches, pkts; |
| }; |
| |
| int data_is_native; // Indicates whether to swap |
| static LIST_HEAD(input_files); // List of all input files |
| static char *idir = "."; // Input directory base |
| static char *odir = "."; // Output directory base |
| static char *obase = "replay"; // Output file base |
| static __u64 max_bunch_tm = (10 * 1000 * 1000); // 10 milliseconds |
| static __u64 max_pkts_per_bunch = 8; // Default # of pkts per bunch |
| static int verbose = 0; // Boolean: output stats |
| static int find_traces = 0; // Boolean: Find traces in dir |
| |
| static char usage_str[] = \ |
| "\n" \ |
| "\t[ -d <dir> : --input-directory=<dir> ] Default: .\n" \ |
| "\t[ -D <dir> : --output-directory=<dir>] Default: .\n" \ |
| "\t[ -F : --find-traces ] Default: Off\n" \ |
| "\t[ -h : --help ] Default: Off\n" \ |
| "\t[ -m <nsec> : --max-bunch-time=<nsec> ] Default: 10 msec\n" \ |
| "\t[ -M <pkts> : --max-pkts=<pkts> ] Default: 8\n" \ |
| "\t[ -o <base> : --output-base=<base> ] Default: replay\n" \ |
| "\t[ -v : --verbose ] Default: Off\n" \ |
| "\t[ -V : --version ] Default: Off\n" \ |
| "\t<dev>... Default: None\n" \ |
| "\n"; |
| |
| #define S_OPTS "d:D:Fhm:M:o:vV" |
| static struct option l_opts[] = { |
| { |
| .name = "input-directory", |
| .has_arg = required_argument, |
| .flag = NULL, |
| .val = 'd' |
| }, |
| { |
| .name = "output-directory", |
| .has_arg = required_argument, |
| .flag = NULL, |
| .val = 'D' |
| }, |
| { |
| .name = "find-traces", |
| .has_arg = no_argument, |
| .flag = NULL, |
| .val = 'F' |
| }, |
| { |
| .name = "help", |
| .has_arg = no_argument, |
| .flag = NULL, |
| .val = 'h' |
| }, |
| { |
| .name = "max-bunch-time", |
| .has_arg = required_argument, |
| .flag = NULL, |
| .val = 'm' |
| }, |
| { |
| .name = "max-pkts", |
| .has_arg = required_argument, |
| .flag = NULL, |
| .val = 'M' |
| }, |
| { |
| .name = "output-base", |
| .has_arg = required_argument, |
| .flag = NULL, |
| .val = 'o' |
| }, |
| { |
| .name = "verbose", |
| .has_arg = no_argument, |
| .flag = NULL, |
| .val = 'v' |
| }, |
| { |
| .name = "version", |
| .has_arg = no_argument, |
| .flag = NULL, |
| .val = 'V' |
| }, |
| { |
| .name = NULL |
| } |
| }; |
| |
| #define ERR_ARGS 1 |
| #define ERR_SYSCALL 2 |
| static inline void fatal(const char *errstring, const int exitval, |
| const char *fmt, ...) |
| { |
| va_list ap; |
| |
| if (errstring) |
| perror(errstring); |
| |
| va_start(ap, fmt); |
| vfprintf(stderr, fmt, ap); |
| va_end(ap); |
| |
| exit(exitval); |
| /*NOTREACHED*/ |
| } |
| |
| /** |
| * match - Return true if this trace is a proper QUEUE transaction |
| * @action: Action field from trace |
| */ |
| static inline int match(__u32 action) |
| { |
| return ((action & 0xffff) == __BLK_TA_QUEUE) && |
| (action & BLK_TC_ACT(BLK_TC_QUEUE)); |
| } |
| |
| /** |
| * usage - Display usage string and version |
| */ |
| static void usage(void) |
| { |
| fprintf(stderr, "Usage: btrecord -- version %s\n%s", |
| my_btversion, usage_str); |
| } |
| |
| /** |
| * write_file_hdr - Seek to and write btrecord file header |
| * @stream: Output file information |
| * @hdr: Header to write |
| */ |
| static void write_file_hdr(struct io_stream *stream, struct io_file_hdr *hdr) |
| { |
| hdr->version = mk_btversion(btver_mjr, btver_mnr, btver_sub); |
| |
| if (verbose) { |
| fprintf(stderr, "\t%s: %llx %llx %llx %llx\n", |
| stream->file_name, |
| (long long unsigned)hdr->version, |
| (long long unsigned)hdr->genesis, |
| (long long unsigned)hdr->nbunches, |
| (long long unsigned)hdr->total_pkts); |
| } |
| |
| fseek(stream->ofp, 0, SEEK_SET); |
| if (fwrite(hdr, sizeof(*hdr), 1, stream->ofp) != 1) { |
| fatal(stream->file_name, ERR_SYSCALL, "Hdr write failed\n"); |
| /*NOTREACHED*/ |
| } |
| } |
| |
| /** |
| * io_bunch_create - Allocate & initialize an io_bunch |
| * @io_stream: IO stream being added to |
| * @pre_stall: Amount of time that this bunch should be delayed by |
| * @start_time: Records current start |
| */ |
| static inline void io_bunch_create(struct io_stream *stream, __u64 start_time) |
| { |
| struct io_bunch *cur = malloc(sizeof(*cur)); |
| |
| memset(cur, 0, sizeof(*cur)); |
| |
| cur->hdr.npkts = 0; |
| cur->hdr.time_stamp = stream->start_time = start_time; |
| |
| stream->cur = cur; |
| } |
| |
| /** |
| * io_bunch_add - Add an IO to the current bunch of IOs |
| * @stream: Per-output file stream information |
| * @spec: IO trace specification |
| * |
| * Returns update bunch information |
| */ |
| static void io_bunch_add(struct io_stream *stream, struct io_spec *spec) |
| { |
| struct io_bunch *cur = stream->cur; |
| struct io_pkt iop = { |
| .sector = spec->sector, |
| .nbytes = spec->bytes, |
| .rw = spec->rw |
| }; |
| |
| assert(cur != NULL); |
| assert(cur->hdr.npkts < BT_MAX_PKTS); |
| assert(stream->last_time == 0 || stream->last_time <= spec->time); |
| |
| cur->pkts[cur->hdr.npkts++] = iop; // Struct copy |
| stream->last_time = spec->time; |
| } |
| |
| /** |
| * rem_input_file - Release resources associated with an input file |
| * @iip: Per-input file information |
| */ |
| static void rem_input_file(struct ifile_info *iip) |
| { |
| list_del(&iip->head); |
| |
| close(iip->ifd); |
| free(iip->file_name); |
| free(iip->devnm); |
| free(iip); |
| } |
| |
| /** |
| * __add_input_file - Allocate and initialize per-input file structure |
| * @cpu: CPU for this file |
| * @devnm: Device name for this file |
| * @file_name: Fully qualifed input file name |
| */ |
| static void __add_input_file(int cpu, char *devnm, char *file_name) |
| { |
| struct ifile_info *iip = malloc(sizeof(*iip)); |
| |
| iip->cpu = cpu; |
| iip->tpkts = 0; |
| iip->genesis = 0; |
| iip->devnm = strdup(devnm); |
| iip->file_name = strdup(file_name); |
| iip->ifd = open(file_name, O_RDONLY); |
| if (iip->ifd < 0) { |
| fatal(file_name, ERR_ARGS, "Unable to open\n"); |
| /*NOTREACHED*/ |
| } |
| |
| list_add_tail(&iip->head, &input_files); |
| } |
| |
| /** |
| * add_input_file - Set up the input file name |
| * @devnm: Device name to use |
| */ |
| static void add_input_file(char *devnm) |
| { |
| struct list_head *p; |
| int cpu, found = 0; |
| |
| __list_for_each(p, &input_files) { |
| struct ifile_info *iip = list_entry(p, struct ifile_info, head); |
| if (strcmp(iip->devnm, devnm) == 0) |
| return; |
| } |
| |
| for (cpu = 0; ; cpu++) { |
| char full_name[MAXPATHLEN]; |
| |
| sprintf(full_name, "%s/%s.blktrace.%d", idir, devnm, cpu); |
| if (access(full_name, R_OK) != 0) |
| break; |
| |
| __add_input_file(cpu, devnm, full_name); |
| found++; |
| } |
| |
| if (!found) { |
| fatal(NULL, ERR_ARGS, "No traces found for %s\n", devnm); |
| /*NOTREACHED*/ |
| } |
| } |
| |
| static void find_input_files(char *idir) |
| { |
| struct dirent *ent; |
| DIR *dir = opendir(idir); |
| |
| if (dir == NULL) { |
| fatal(idir, ERR_ARGS, "Unable to open %s\n", idir); |
| /*NOTREACHED*/ |
| } |
| |
| while ((ent = readdir(dir)) != NULL) { |
| char *p, *dsf = malloc(256); |
| |
| if (strstr(ent->d_name, ".blktrace.") == NULL) |
| continue; |
| |
| dsf = strdup(ent->d_name); |
| p = index(dsf, '.'); |
| assert(p != NULL); |
| *p = '\0'; |
| add_input_file(dsf); |
| free(dsf); |
| } |
| |
| closedir(dir); |
| } |
| |
| /** |
| * handle_args - Parse passed in argument list |
| * @argc: Number of arguments in argv |
| * @argv: Arguments passed in |
| * |
| * Does rudimentary parameter verification as well. |
| */ |
| void handle_args(int argc, char *argv[]) |
| { |
| int c; |
| |
| while ((c = getopt_long(argc, argv, S_OPTS, l_opts, NULL)) != -1) { |
| switch (c) { |
| case 'd': |
| idir = optarg; |
| if (access(idir, R_OK | X_OK) != 0) { |
| fatal(idir, ERR_ARGS, |
| "Invalid input directory specified\n"); |
| /*NOTREACHED*/ |
| } |
| break; |
| |
| case 'D': |
| odir = optarg; |
| if (access(odir, R_OK | X_OK) != 0) { |
| fatal(odir, ERR_ARGS, |
| "Invalid output directory specified\n"); |
| /*NOTREACHED*/ |
| } |
| break; |
| |
| case 'F': |
| find_traces = 1; |
| break; |
| |
| case 'h': |
| usage(); |
| exit(0); |
| /*NOTREACHED*/ |
| |
| case 'm': |
| max_bunch_tm = (__u64)atoll(optarg); |
| if (max_bunch_tm < 1) { |
| fprintf(stderr, "Invalid bunch time %llu\n", |
| (unsigned long long)max_bunch_tm); |
| exit(ERR_ARGS); |
| /*NOTREACHED*/ |
| } |
| break; |
| |
| case 'M': |
| max_pkts_per_bunch = (__u64)atoll(optarg); |
| if (!((1 <= max_pkts_per_bunch) && |
| (max_pkts_per_bunch < 513))) { |
| fprintf(stderr, "Invalid max pkts %llu\n", |
| (unsigned long long)max_pkts_per_bunch); |
| exit(ERR_ARGS); |
| /*NOTREACHED*/ |
| } |
| break; |
| |
| case 'o': |
| obase = optarg; |
| break; |
| |
| case 'V': |
| fprintf(stderr, "btrecord -- version %s\n", |
| my_btversion); |
| fprintf(stderr, " Built on %s\n", build_date); |
| exit(0); |
| /*NOTREACHED*/ |
| |
| case 'v': |
| verbose++; |
| break; |
| |
| default: |
| usage(); |
| fatal(NULL, ERR_ARGS, "Invalid command line\n"); |
| /*NOTREACHED*/ |
| } |
| } |
| |
| while (optind < argc) |
| add_input_file(argv[optind++]); |
| |
| if (find_traces) |
| find_input_files(idir); |
| |
| if (list_len(&input_files) == 0) { |
| fatal(NULL, ERR_ARGS, "Missing required input file name(s)\n"); |
| /*NOTREACHED*/ |
| } |
| } |
| |
| /** |
| * next_io - Retrieve next Q trace from input stream |
| * @iip: Per-input file information |
| * @spec: IO specifier for trace |
| * |
| * Returns 0 on end of file, 1 if valid data returned. |
| */ |
| static int next_io(struct ifile_info *iip, struct io_spec *spec) |
| { |
| ssize_t ret; |
| __u32 action; |
| __u16 pdu_len; |
| struct blk_io_trace t; |
| |
| again: |
| ret = read(iip->ifd, &t, sizeof(t)); |
| if (ret < 0) { |
| fatal(iip->file_name, ERR_SYSCALL, "Read failed\n"); |
| /*NOTREACHED*/ |
| } |
| else if (ret == 0) |
| return 0; |
| else if (ret < (ssize_t)sizeof(t)) { |
| fprintf(stderr, "WARNING: Short read on %s (%d)\n", |
| iip->file_name, (int)ret); |
| return 0; |
| } |
| |
| if (data_is_native == -1) |
| check_data_endianness(t.magic); |
| |
| assert(data_is_native >= 0); |
| if (data_is_native) { |
| spec->time = t.time; |
| spec->sector = t.sector; |
| spec->bytes = t.bytes; |
| action = t.action; |
| pdu_len = t.pdu_len; |
| } |
| else { |
| spec->time = be64_to_cpu(t.time); |
| spec->sector = be64_to_cpu(t.sector); |
| spec->bytes = be32_to_cpu(t.bytes); |
| action = be32_to_cpu(t.action); |
| pdu_len = be16_to_cpu(t.pdu_len); |
| } |
| |
| |
| if (pdu_len) { |
| char buf[pdu_len]; |
| |
| ret = read(iip->ifd, buf, pdu_len); |
| if (ret < 0) { |
| fatal(iip->file_name, ERR_SYSCALL, "Read PDU failed\n"); |
| /*NOTREACHED*/ |
| } |
| else if (ret < (ssize_t)pdu_len) { |
| fprintf(stderr, "WARNING: Short PDU read on %s (%d)\n", |
| iip->file_name, (int)ret); |
| return 0; |
| } |
| } |
| |
| iip->tpkts++; |
| if (!match(action)) |
| goto again; |
| |
| spec->rw = (action & BLK_TC_ACT(BLK_TC_READ)) ? 1 : 0; |
| if (verbose > 1) |
| fprintf(stderr, "%2d: %10llu+%10llu (%d) @ %10llx\n", |
| iip->cpu, (long long unsigned)spec->sector, |
| (long long unsigned)spec->bytes / 512LLU, |
| spec->rw, (long long unsigned)spec->time); |
| |
| if (iip->genesis == 0) { |
| iip->genesis = spec->time; |
| if (verbose > 1) |
| fprintf(stderr, "\tSetting new genesis: %llx(%d)\n", |
| (long long unsigned)iip->genesis, iip->cpu); |
| } |
| else if (iip->genesis > spec->time) |
| fatal(NULL, ERR_SYSCALL, |
| "Time inversion? %llu ... %llu\n", |
| (long long unsigned )iip->genesis, |
| (long long unsigned )spec->time); |
| |
| return 1; |
| } |
| |
| /** |
| * bunch_output_hdr - Output bunch header |
| */ |
| static inline void bunch_output_hdr(struct io_stream *stream) |
| { |
| struct io_bunch_hdr *hdrp = &stream->cur->hdr; |
| |
| assert(0 < hdrp->npkts && hdrp->npkts <= BT_MAX_PKTS); |
| if (fwrite(hdrp, sizeof(struct io_bunch_hdr), 1, stream->ofp) != 1) { |
| fatal(stream->file_name, ERR_SYSCALL, "fwrite(hdr) failed\n"); |
| /*NOTREACHED*/ |
| } |
| |
| if (verbose) { |
| __u64 off = hdrp->time_stamp - stream->iip->genesis; |
| |
| assert(stream->vfp); |
| fprintf(stream->vfp, "------------------\n"); |
| fprintf(stream->vfp, "%4llu.%09llu %3llu\n", |
| (unsigned long long)off / (1000 * 1000 * 1000), |
| (unsigned long long)off % (1000 * 1000 * 1000), |
| (unsigned long long)hdrp->npkts); |
| fprintf(stream->vfp, "------------------\n"); |
| } |
| } |
| |
| /** |
| * bunch_output_pkt - Output IO packets |
| */ |
| static inline void bunch_output_pkts(struct io_stream *stream) |
| { |
| struct io_pkt *p = stream->cur->pkts; |
| size_t npkts = stream->cur->hdr.npkts; |
| |
| assert(0 < npkts && npkts <= BT_MAX_PKTS); |
| if (fwrite(p, sizeof(struct io_pkt), npkts, stream->ofp) != npkts) { |
| fatal(stream->file_name, ERR_SYSCALL, "fwrite(pkts) failed\n"); |
| /*NOTREACHED*/ |
| } |
| |
| if (verbose) { |
| size_t i; |
| |
| assert(stream->vfp); |
| for (i = 0; i < npkts; i++, p++) |
| fprintf(stream->vfp, "\t%1d %10llu\t%10llu\n", |
| p->rw, |
| (unsigned long long)p->sector, |
| (unsigned long long)p->nbytes / 512); |
| } |
| } |
| |
| /** |
| * stream_flush - Flush current bunch of IOs out to the output stream |
| * @stream: Per-output file stream information |
| */ |
| static void stream_flush(struct io_stream *stream) |
| { |
| struct io_bunch *cur = stream->cur; |
| |
| if (cur) { |
| if (cur->hdr.npkts) { |
| assert(cur->hdr.npkts <= BT_MAX_PKTS); |
| bunch_output_hdr(stream); |
| bunch_output_pkts(stream); |
| |
| stream->bunches++; |
| stream->pkts += cur->hdr.npkts; |
| } |
| free(cur); |
| } |
| } |
| |
| /** |
| * bunch_done - Returns true if current bunch is either full, or next IO is late |
| * @stream: Output stream information |
| * @spec: IO trace specification |
| */ |
| static inline int bunch_done(struct io_stream *stream, struct io_spec *spec) |
| { |
| if (stream->cur->hdr.npkts >= max_pkts_per_bunch) |
| return 1; |
| |
| if ((spec->time - stream->start_time) > max_bunch_tm) |
| return 1; |
| |
| return 0; |
| } |
| |
| /** |
| * stream_add_io - Add an IO trace to the current stream |
| * @stream: Output stream information |
| * @spec: IO trace specification |
| */ |
| static void stream_add_io(struct io_stream *stream, struct io_spec *spec) |
| { |
| |
| if (stream->cur == NULL) |
| io_bunch_create(stream, spec->time); |
| else if (bunch_done(stream, spec)) { |
| stream_flush(stream); |
| io_bunch_create(stream, spec->time); |
| } |
| |
| io_bunch_add(stream, spec); |
| } |
| |
| /** |
| * stream_open - Open output stream for specified input stream |
| * @iip: Per-input file information |
| */ |
| static struct io_stream *stream_open(struct ifile_info *iip) |
| { |
| char ofile_name[MAXPATHLEN]; |
| struct io_stream *stream = malloc(sizeof(*stream)); |
| struct io_file_hdr io_file_hdr = { |
| .genesis = 0, |
| .nbunches = 0, |
| .total_pkts = 0 |
| }; |
| |
| memset(stream, 0, sizeof(*stream)); |
| |
| sprintf(ofile_name, "%s/%s.%s.%d", odir, iip->devnm, obase, iip->cpu); |
| stream->ofp = fopen(ofile_name, "w"); |
| if (!stream->ofp) { |
| fatal(ofile_name, ERR_SYSCALL, "Open failed\n"); |
| /*NOTREACHED*/ |
| } |
| |
| stream->iip = iip; |
| stream->cur = NULL; |
| stream->bunches = stream->pkts = 0; |
| stream->last_time = 0; |
| stream->file_name = strdup(ofile_name); |
| |
| write_file_hdr(stream, &io_file_hdr); |
| |
| if (verbose) { |
| char vfile_name[MAXPATHLEN]; |
| |
| sprintf(vfile_name, "%s/%s.%s.%d.rec", odir, iip->devnm, |
| obase, iip->cpu); |
| stream->vfp = fopen(vfile_name, "w"); |
| if (!stream->vfp) { |
| fatal(vfile_name, ERR_SYSCALL, "Open failed\n"); |
| /*NOTREACHED*/ |
| } |
| |
| stream->vfn = strdup(vfile_name); |
| } |
| |
| data_is_native = -1; |
| return stream; |
| } |
| |
| /** |
| * stream_close - Release resources associated with an output stream |
| * @stream: Stream to release |
| */ |
| static void stream_close(struct io_stream *stream) |
| { |
| struct io_file_hdr io_file_hdr = { |
| .genesis = stream->iip->genesis, |
| .nbunches = stream->bunches, |
| .total_pkts = stream->pkts |
| }; |
| |
| stream_flush(stream); |
| write_file_hdr(stream, &io_file_hdr); |
| fclose(stream->ofp); |
| |
| if (verbose && stream->bunches) { |
| fprintf(stderr, |
| "%s:%d: %llu pkts (tot), %llu pkts (replay), " |
| "%llu bunches, %.1lf pkts/bunch\n", |
| stream->iip->devnm, stream->iip->cpu, |
| (unsigned long long)stream->iip->tpkts, |
| (unsigned long long)stream->pkts, |
| (unsigned long long)stream->bunches, |
| (double)(stream->pkts) / (double)(stream->bunches)); |
| |
| fclose(stream->vfp); |
| free(stream->vfn); |
| } |
| |
| free(stream->file_name); |
| free(stream); |
| } |
| |
| /** |
| * process - Process one input file to an output file |
| * @iip: Per-input file information |
| */ |
| static void process(struct ifile_info *iip) |
| { |
| struct io_spec spec; |
| struct io_stream *stream; |
| |
| stream = stream_open(iip); |
| while (next_io(iip, &spec)) |
| stream_add_io(stream, &spec); |
| stream_close(stream); |
| |
| rem_input_file(iip); |
| } |
| |
| /** |
| * main - |
| * @argc: Number of arguments |
| * @argv: Array of arguments |
| */ |
| int main(int argc, char *argv[]) |
| { |
| struct list_head *p, *q; |
| |
| handle_args(argc, argv); |
| list_for_each_safe(p, q, &input_files) |
| process(list_entry(p, struct ifile_info, head)); |
| |
| return 0; |
| } |