Книга: Разработка приложений в среде Linux. Второе издание

10.7.1. Запуск внешних программ с помощью ladsh

10.7.1. Запуск внешних программ с помощью ladsh

Вот первая (и самая простая) версия ladsh, называемая ladsh1.

  1: /*ladsh1.c*/
  2:
  3: #include <ctype.h>
  4: #include <errno.h>
  5: #include <fcntl.h>
  6: #include <signal.h>
  7: #include <stdio.h>
  8: #include <stdlib.h>
  9: #include <string.h>
 10: #include <sys/ioctl.h>
 11: #include <sys/wait.h>
 12: #include <unistd.h>
 13:
 14: #define MAX_COMMAND_LEN 250 /* максимальная длина отдельной
 15:                                командной строки */
 16: #define JOB_STATUS_FORMAT "[%d]%-22s%.40sn"
 17:
 18: struct jobSet {
 19:  struct job *head; /* заголовок списка запущенных заданий */
 20:  struct job *fg;   /* текущее задание переднего плана */
 21: };
 22:
 23: struct childProgram {
 24:  pid_t Pid;   /* 0 на выходе */
 25:  char **argv; /* имя программы с аргументами */
 26: };
 27:
 28: struct job {
 29:  int job Id;       /* номер задания */
 30:  int numProgs;     /* общее кол-во программ в задании */
 31:  int runningProgs; /* кол-во работающих программ */
 32:  char *text;       /* имя задания */
 33:  char *cmdBuf;     /* буфер различных argv */
 34:  pid_t pgrp;       /* идентификатор группы процессов задания */
 35:  struct childProgram *progs; /* массив программ в задании */
 36:  struct job *next; /* для слежения за фоновыми программами */
 37: };
 38:
 39: void freeJob(struct job *cmd) {
 40:  int i;
 41:
 42:  for (i=0; i<cmd->numProgs; i++) {
 43:   free (cmd->progs[i].argv);
 44:  }
 45:  free(cmd->progs);
 46:  if (cmd->text) free(cmd->text);
 47:   free(cmd->cmdBuf);
 48:  }
 49:
 50:  int getCommand(FILE *source, char *command) {
 51:  if (source == stdin) {
 52:   printf("#");
 53:   fflush(stdout);
 54:  }
 55:
 56:  if (!fgets(command, MAX_COMMAND_LEN, source)) {
 57:   if (source==stdin) printf("n");
 58:   return 1;
 59:  }
 60:
 61:  /* удалить завершающий перевод строки */
 62:  command[strlen(command) - 1] = '';
 63:
 64:  return 0;
 65: }
 66:
 67: /* Возвратить cmd->numProgs как 0, если нет никаких команд (то есть пустая
 68:    строка). Если найдена правильная команда, commandPtr устанавливается в
 69:    указатель на начало следующей команды (если исходная команда имеет более
 70:    одного задания, ассоциированного с ней) или NULL, если
 71:    больше нет команд.*/
 72: int parseCommand(char **commandPtr, struct job *job, int *isBg) {
 73:  char *command;
 74:  char *returnCommand = NULL;
 75:  char *src, *buf;
 76:  int argc = 0;
 77:  int done = 0;
 78:  int argvAlloced;
 79:  char quote = '';
 80:  int count;
 81:  struct childProgram *prog;
 82:
 83:  /* Пропустить ведущие пробелы */
 84:  while(**commandPtr && isspace(**commandPtr)) (*commandPtr)++;
 85:
 86:  /* здесь обрабатываются пустые строки и ведущие символы '#' */
 87:  if (!**commandPtr || (**commandPtr=='#')) {
 88:   job->numProgs = 0;
 89:   *commandPtr = NULL;
 90:   return 0;
 91:  }
 92:
 93:  *isBg = 0;
 94:  job->numProgs = 1;
 95:  job->progs = malloc(sizeof(*job->progs));
 96:
 97:  /* Мы устанавливаем элементы argv в указатели внутри строки.
 98:     Память освобождается freeJob().
 99:
100:     Получение чистой памяти позволяет далее иметь дело с
101:     NULL-завершающимися вещами и делает все остальное немного
102:     яснее (к тому же, это добавляет эффективности) */
103:  job->cmdBuf = command = calloc(1, strlen(*commandPtr) + 1);
104:  job->text = NULL;
105:
106:  prog = job->progs;
107:
108:  argvAlloced = 5;
109:  prog->argv = malloc(sizeof(*prog->argv) * argvAlloced);
110:  prog->argv[0] = job->cmdBuf;
111:
112:  buf = command;
113:  src = *commandPtr;
114:  while (*src && !done) {
115:   if (quote==*src) {
116:    quote='';
117:   } else if (quote) {
118:    if (*src == '') {
119:     src++;
120:     if (!*src) {
121:      fprintf(stderr,
122:       "ожидается символ послеn");
123:      freeJob(job);
124:      return 1;
125:     }
126:
127:     /* в оболочке, "'" должно породить ' */
128:     if (*src != quote) *buf++='';
129:    }
130:    *buf++ = *src;
131:   } else if (isspace(*src)) {
132:    if (*prog->argv[argc]) {
133:     buf++, argc++;
134:     /* +1 здесь оставляет место для NULL,
135:        которым завершается argv */
136:     if ((argc+1) == argvAlloced) {
137:      argvAlloced += 5;
138:      prog->argv = realloc(prog->argv,
139:       sizeof(*prog->argv)*argvAlloced);
140:     }
141:     prog->argv[argc]=buf;
142:    }
143:   } else switch(*src) {
144:   case '"':
145:   case ''':
146:    quote = *src;
147:    break;
148:
149:   case '#' : /* комментарий */
150:    done=1;
151:    break;
152:
153:   case '&': /* фоновый режим */
154:    *isBg = 1;
155:   case ';': /* множественные команды */
156:    done=1;
157:    return Command = *commandPtr + (src - *commandPtr) + 1;
158:    break;
159:
160:   case '' :
161:    src++;
162:    if (!*src) {
163:     freeJob(job);
164:     fprintf(stderr, "ожидается символ после n");
165:     return 1;
166:    }
167:    /* двигаться дальше */
168:   default:
169:    *buf++=*src;
170:   }
171:
172:   src++;
173:  }
174:
175:  if (*prog->argv[argc]) {
176:   argc++;
177:  }
178:  if (!argc) {
179:   freeJob(job);
180:   return 0;
181:  }
182:  prog->argv[argc]=NULL;
183:
184:  if (!returnCommand) {
185:   job->text = malloc(strlen(*commandPtr) + 1);
186:   strcpy(job->text,*commandPtr);
187:  } else {
188:   /* Это оставляет хвостовые пробелы, что несколько излишне */
189:
190:   count = returnCommand - *commandPtr;
191:   job->text = malloc(count + 1);
192:   strncpy(job->text,*commandPtr,count);
193:   job->text[count] = '';
194:  }
195:
196:  *commandPtr = returnCommand;
197:
198:  return 0;
199: }
200:
201: int runCommand(struct jobnewJob, struct jobSet *jobList,
202:  intinBg) {
203:  struct job *job;
204:
205:  /* обходной путь "вручную" - мы не используем fork(),
206:     поэтому не можем легко реализовать фоновый режим */
207:  if (!strcmp(newJob.progs[0].argv[0], "exit")) {
208:   /* это должно вернуть реальный код возврата */
209:   exit(0);
210:  } else if(!strcmp(newJob.progs[0].argv[0], "jobs")) {
211:   for (job = jobList->head; job; job = job->next)
212:    printf(JOB_STATUS_FORMAT, job->jobId, "Работаю",
213:     job->text);
214:   return 0;
215:  }
216:
217:  /* у нас пока только одна программа на дочернее задание,
218:     потому это просто */
219:  if (!(newJob.progs[0].pid = fork())) {
220:   execvp(newJob.progs[0].argv[0],newJob.progs[0].argv);
221:   fprintf(stderr, "exec() для %s потерпела неудачу: %sn",
222:    newJob.progs[0].argv[0],
223:   strerror(errno));
224:   exit(1);
225:  }
226:
227:  /* поместить дочернюю программу в отдельную группу процессов */
228:  setpgid(newJob.progs[0].pid,newJob.progs[0].pid);
229:
230:  newJob.pgrp = newJob.progs[0].pid;
231:
232:  /* найти идентификатор для задания */
233:  newJob.jobld = 1;
234:  for (job = jobList->head; job; job = job->next)
235:   if (job->jobId >= newJob.jobId)
236:    newJob.jobId = job->jobId+1;
237:
238:  /* задание для списка заданий */
239:  if (!jobList->head) {
240:   job = jobList->head = malloc(sizeof(*job));
241:  } else {
242:   for (job = jobList->head; job->next; job = job->next);
243:   job->next = malloc(sizeof(*job));
244:   job = job->next;
245:  }
246:
247:  *job = newJob;
248:  job->next = NULL;
249:  job->runningProgs = job->numProgs;
250:
251:  if (inBg) {
252:   /* мы не ждем завершения фоновых заданий - добавить
253:      в список фоновых заданий и оставить в покое */
254:
255:   printf("[%d]%dn", job->jobId,
256:    newJob.progs[newJob.numProgs-1].pid);
257:  } else {
258:   jobList->fg=job;
259:
260:   /* переместить новую группу процессов на передний план */
261:
262:   if (tcsetpgrp(0,newJob.pgrp))
263:    perror("tcsetpgrp");
264:  }
265:
266:  return 0;
267: }
268:
269: void removeJob(struct jobSet *jobList, struct job *job) {
270:  struct job *prevJob;
271:
272:  freeJob(job);
273:  if (job == jobList->head) {
274:   jobList->head=job->next;
275:  } else {
276:   prevJob = jobList->head;
277:   while (prevJob->next != job) prevJob = prevJob->next;
278:   prevJob->next=job->next;
279:  }
280:
281:  free(job);
282: }
283:
284: /* Проверить, завершился ли какой-то из фоновых процессов -
285:    если да, выяснить, почему и определить, завершилось ли задание */
286: void checkJobs(struct jobSet *jobList) {
287:  struct job *job;
288:  pid_t childpid;
289:  int status;
290:  int progNum;
291:
292:  while ((childpid = waitpid(-1, &status, WNOHANG))>0) {
293:   for (job = jobList->head;job;job = job->next) {
294:    progNum = 0;
295:    while (progNum<job->numProgs &&
296:     job->progs[progNum].pid != childpid)
297:     progNum++;
298:    if (progNum<job->numProgs) break;
299:   }
300:
301:   job->runningProgs--;
302:   job->progs[progNum].pid = 0;
303:
304:   if (!job->runningProgs) {
305:    printf(JOB_STATUS_FORMAT,job->jobId,"Готово",
306:     job->text);
307:    removeJob(jobList, job);
308:   }
309:  }
310:
311:  if (childpid == -1 && errno!= ECHILD)
312:   perror("waitpid");
313:  }
314:
315:  int main(int argc, const char **argv) {
316:   char command [MAX_COMMAND_LEN + 1];
317:   char *nextCommand = NULL;
318:   struct jobSetjobList = {NULL, NULL};
319:   struct jobnewJob;
320:   FILE *input = stdin;
321:   int i;
322:   int status;
323:   int inBg;
324:
325:   if (argc>2) {
326:    fprintf(stderr,"Непредвиденные аргументы; использование: ladsh1 "
327:     "<команды>n");
328:    exit(1);
329:   } else if (argc == 2) {
330:    input = fopen(argv[1], "r");
331:    if (!input) {
332:     perror("fopen");
333:     exit(1);
334:    }
335:   }
336:
337:   /* не обращать внимания на этот сигнал; он только вводит
338:      в заблуждение и не имеет особого значения для оболочки */
339:   signal(SIGTTOU, SIG_IGN);
340:
341:   while(1) {
342:   if (!jobList.fg) {
343:    /* нет заданий переднего плана */
344:
345:    /* проверить, завершились ли какие-то фоновые процессы */
346:    checkJobs(&jobList);
347:
348:    if (!nextCommand) {
349:     if (getCommand(input, command)) break;
350:     nextCommand=command;
351:    }
352:
353:    if (!parseCommand(&nextCommand, &newJob, &inBg) &&
354:     newJob.numProgs) {
355:     runCommand(newJob,&jobList,inBg);
356:    }
357:   } else {
358:    /* задание выполняется на переднем плане; ждать завершения */
359:    i = 0;
360:    while (!jobList.fg->progs[i].pid) i++;
361:
362:    waitpid(jobList.fg->progs[i].pid,&status,0);
363:
364:    jobList.fg->runningProgs--;
365:    jobList.fg->progs[i].pid=0;
366:
367:    if (!jobList.fg->runningProgs) {
368:     /* дочернее завершилось */
369:
370:     removeJob(&jobList, jobList.fg);
371:     jobList.fg = NULL;
372:
373:     /* переместить оболочку на передний план */
374:     if (tcsetpgrp(0, getpid()))
375:      perror("tcsetpgrp");
376:    }
377:   }
378:  }
379:
380:  return 0;
381: }

Эта версия не делает ничего, кроме запуска внешней программы с аргументами, поддержки комментариев стиля # (все, что следует за символом #, игнорируется), и позволяет программам выполняться в фоновом режиме. Она работает как интерпретатор простых сценариев оболочки, написанных в нотации #!, но ничего сверх этого не делает. Она разработана в качестве имитации обычного интерпретатора оболочки, используемого в системах Linux, несмотря на то, что в значительной степени упрощена.

Прежде всего, взглянем на структуры данных, которые здесь используются. На рис. 10.2 показаны структуры данных, используемые в ladsh1.с для отслеживания запускаемых дочерних процессов, на примере применения программы grep в фоновом режиме и links — в режиме переднего плана, struct jobSet описывает набор функционирующих заданий. Он содержит связный список заданий и указатель на текущее задание, выполняемое на переднем плане. Если такового нет, то указатель равен NULL, ladsh1.с использует struct jobSet для того, чтобы отслеживать задания, выполняемые в данный момент в фоновом режиме.


Рис. 10.2. Структуры данных, описывающие задания для ladsh1.с
struct childProgram
описывает отдельную выполняемую программу. Это не совсем то же самое, что задание — в конце концов, каждое задание может состоять из нескольких программ, связанных по программным каналам. Для каждой дочерней программы ladsh отслеживает pid, имя программы и аргументы командной строки. Первый элемент argv, argv[0], содержит имя запущенной программы, которое передается также потомку в виде первого аргумента.

Множество программ объединяется в одно задание с помощью struct job. Каждое задание имеет уникальный идентификатор в оболочке, соответствующее количество программ, составляющих задание (хранимых в progs, указателе на массив struct childProgram), а также указатель на другое (следующее) задание, что позволяет объединять их вместе в связный список (который описывает struct jobSet). Задание также отслеживает, сколько отдельных программ составляет его, и сколько их них все еще выполняются (поскольку не все компоненты задания могут завершаться одновременно). Остальные два члена — text и cmdBuf — служат в качестве буферов для хранения различных строк, которые используются структурами struct childProgram, содержащимися в задании.

Большая часть struct jobSet состоит из динамически распределенной памяти, которая должна быть освобождена по завершении задания. Первая функция в ladsh1.с, freeJob(), освобождает память, использованную заданием.

Следующая функция, getCommand(), получает команду, введенную пользователем, и возвращает строку. Если команды читаются из файла, то никакого приглашения не выводится (вот почему код сравнивает входной файловый поток со stdin).

parseCommand() разбивает строку команды в структуру struct job для использования в ladsh. Первый аргумент — это указатель на указатель на команду. Если в строке множество команд, он переставляется на начало следующей команды. Он устанавливается в NULL, когда завершается разбор последней команды в строке. Это позволяет parseCommand() разбирать только одну команду при каждом вызове и дает возможность вызывающей функции просто разбирать строку за несколько вызовов. Следует отметить, что несколько программ, объединенных каналами, не рассматриваются как отдельные команды — независимыми друг от друга считаются только команды, разделенные символами ; или &. Поскольку parseCommand() — это просто пример разбора строк, мы не будем углубляться в детали ее работы.

Функция runCommand() отвечает за запуск отдельного задания. Она принимает структуру struct job, описывающую запускаемое задание, список заданий, выполняющихся в данный момент, а также флаг, указывающий, должно ли задание выполняться в фоновом режиме или же на переднем плане.

Пока ladsh не поддерживает каналов, поэтому каждое задание может состоять только из одной программы (хотя большая часть инфраструктуры, поддерживающей каналы, уже присутствует в ladsh1.с). Если пользователь запускает exit, происходит немедленный выход из программы. Это пример встроенной команды, которую выполняет сама оболочка для обеспечения правильного поведения. Другая встроенная команда — jobs — также здесь реализована.

Если же команда не является встроенной, необходимо выполнить дочернюю команду. Поскольку каждое задание состоит только из одной программы (до тех пор, пока не будут реализованы каналы), это сделать достаточно просто.

219: if (!(newJob.progs[0].pid = fork())) {
220:  execvp(newJob.progs[0].argv[0], newJob.progs[0].argv);
221:  fprintf(stderr, "exec() для %s потерпела неудачу: %sn",
222:   newJob.progs[0].argv[0],
223:  strerror(errno));
224:  exit(1);
225: }

Во-первых, с помощью fork() порождается дочерний процесс. Родитель сохраняет идентификатор pid дочернего процесса в newJob.progs[0].pid, тогда как дочерний процесс сохраняет там 0 (помните, что родитель и потомок имеют разные образы памяти, хотя изначально они и содержат одинаковую информацию). В результате управление в дочернем процессе входит в тело оператора if, в то время как родитель пропускает его. Дочерний немедленно запускает новую программу с помощью вызова execvp(). Если ему этот вызов не удается, печатается сообщение об ошибке и работа завершается. Это все необходимо, чтобы породить простой дочерний процесс.

После порождения дочернего процесса родитель помещает его в его собственную группу и записывает задание в список запущенных заданий. Если процесс должен выполняться на переднем плане (не в фоне), родитель вносит дочерний процесс в группу процессов переднего плана управляющего терминала, на котором работает командная оболочка.

Следующая функция, checkJobs(), ищет фоновые задания, которые были завершены, и соответствующим образом чистит список работающих заданий. Для каждого процесса, который был завершен (помните, что waitpid() возвращает только информацию о завершенных процессах, если только не было указано WUNTRACED), оболочка делает следующие вещи.

1. Ищет задание, частью которого является процесс.

2. Помечает программу как завершенную (устанавливая сохраненный pid равным 0) и уменьшает количество работающих программ в задании на единицу.

Если задание, содержавшее завершенный процесс, не имеет других работающих процессов, что всегда верно для данной версии ladsh, оболочка печатает сообщение пользователю о том, что процесс завершен, и удаляет задание из списка фоновых процессов.

Процедура main() из ladsh1.с контролирует поток управления оболочки. Если при ее запуске ей передан аргумент, он трактуется как имя файла, из которого нужно читать последовательность команд. В противном случае в качестве источника команд используется stdin. Затем программа игнорирует сигнал SIGTTOU. Это элемент "магии" управления заданиями, который обеспечивает, что все происходит гладко. Смысл этого будет пояснен в главе 15. Пока что это только скелет.

Остаток функции main() составляет главный цикл программы. Условие выхода из цикла не предусмотрено. Программа завершается вызовом exit() внутри функции runCommand().

Переменная nextCommand указывает на исходное (не разобранное) строковое представление следующей команды, которая должна быть выполнена, либо NULL, если команда должна быть прочитана из входного файла, коим обычно является stdin. Когда никакое задание не выполняется на переднем плане, ladsh вызывает checkJobs() для проверки выполняющихся фоновых заданий, читает следующую команду из входного файла, если nextCommand равно NULL, затем разбирает и выполняет следующую команду.

Когда выполняется задание переднего плана, ladsh1.с ожидает завершения одного из процессов задания переднего плана. Когда все процессы задания переднего плана завершены, задание исключается из списка запущенных заданий и ladsh1.с читает следующую команду, как описано выше.

Оглавление книги


Генерация: 1.291. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз