Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Состояние выполнения в состояние готовности
В этой ситуации состояние задачи не изменяется, даже если сама задача претерпевает изменение. Абстрактное состояние процесса поможет нам понять, что происходит. Как и в предыдущем случае, процесс переходит из состояния выполнения в состояние готовности, когда процесс переходит из состояния выполнения на процессоре и помещается в очередь выполнения. TASK_RUNNING в TASKJIUNNING Так как в Linux нет специального состояния для задачи, контекст которой выполняется на процессоре, задача в Linux в этом случае не претерпевает перехода между состояниями и остается в состоянии TASK_RUNNING. Планировщик выбирает, когда переключить эту задачу из состояния выполнения и поместить ее в очередь выполнения в соответствии со временем, потраченным задачей на выполнение и ее приоритетом. (Подробности описываются в гл. 7.) Состояние выполнения в состояние блокировки Когда процесс блокируется, он может быть в одном из следующих состояний: TASK_ INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, TASK_ZOMBIE ИЛИ TASK_STOPPED. Теперь опишем, как задача переходит из состояния TASK_RUNNING в каждое из этих состояний, как описано в табл. 3.7. TASKJIUNNING в TASKJNTERRUPTIBLE Это состояние обычно вызывается с помощью блокирования функций ввода-вывода, которые ожидают поступления сообщения или ресурса. Что это значит для задачи, находящейся в состоянии TASK_INTERRUPTIBLE? Она просто остается в очереди выполнения, так как она не готова для выполнения. Задача в состоянии TASK_INTERRUPTIBLE просыпается, если ее ресурс становится доступным (время или аппаратура) или поступает Жизненный цикл процесса сигнал. Завершение оригинального системного вызова зависит от реализации обработчика прерывания. В примере кода дочерний процесс получает доступ к файлу на диске. Драйвер диска определяет, когда устройство станет доступным и к данным можно будет получить доступ. Поэтому код драйвера будет выглядеть примерно следующим образом: while(1) { if(resource_available) break(); set_current_state(TASK_INTERRUPTIBLE); schedule(); } set_current_state(TASK_RUNNING); В этом примере процесс входит в состояние TASK_INTERRUPTIBLE за время, в течение которого выполняется вызов open (). В этой точке он выходит из состояния выполнения с помощью вызова schedule (), а другой процесс из очереди выполнения становится выполняемым процессом. После того как ресурс становится доступным, процесс удаляется из цикла и его состояние изменяется на TASK_RUNNING, которое помещает его обратно в очередь обработки. После этого он ждет, пока планировщик не решит запустить процесс на выполнение. Следующий листинг демонстрирует функцию interruptible_sleep_on (), которая устанавливает задачу в состояние TASK_INTERRUPTIBLE. kernel/sched.с 2504 void interruptible_sleep_on(wait_queue_head_t *q) 2505 { 2506 SLEEP_ON_VAR 2508 current-> state = TASK_INTERRUPTIBLE; 2509 2510 SLEEP_ON_HEAD 2511 scheduleO; 2 512 SLEEP_ON_TAIL 2513 } Макросы SLEEP_ON_HEAD и SLEEP_ON_TAIL заботятся о добавлении и удалении задачи из очереди ожидания (см. раздел «Очередь ожидания» в этой главе). Макрос SLEEP_ON_VAR инициализирует запись о задаче в очереди ожидания, которая добавляется в очередь ожидания. Глава 3 • Процессы: принципиальная модель выполнения TASK_RUNNING в TASKJJNINTERRUPTIBLE Состояние TASK__UNINTERRUPTIBLE похоже на TASK_INTERRUPTIBLE за исключением того, что процесс не формирует сигналы, получаемые, когда он находится в режиме ядра. Это состояние также является состоянием по умолчанию, в которое задача устанавливается при инициализации в процессе ее создания с помощью do_fork(). Функция sleep_on () устанавливает задачу в состояние TASK_UNINTERRUPTIBLE. kernel/sched. с 2 545 long fastcall _____ sched sleep_on(wait_queue_head_t *q) 2546 { 2 547 SLEEP_ON_VAR 2549 current-> state = TASK_UNINTERRUPTIBLE; 2551 SLEEP_ON_HEAD 2552 schedule*); 2553 SLEEP_ON_TAIL 2554 2555 return timeout; 2556 ) Эта функция устанавливает задачу в очередь ожидания, устанавливает ее состояние и вызывает планировщик. TASKJOJNNING в TASKJZOMBIE Процесс в состоянии TASKJZOMBIE называется зомби-процессом. Каждый процесс в течение своего жизненного цикла проходит через это состояние. Длительность времени, в течение которого процесс остается в этом состоянии, зависит от родителя. Чтобы это понять, представьте, что в UNIX-системах каждый процесс может получить статус выхода дочернего процесса с помощью вызовов wait () или waitpid () (см. раздел «Оповещение родителей и sys_wait4()»). Поэтому родительскому процессу должен быть доступен минимум информации, даже когда дочерний процесс уничтожен. Оставлять процесс живым только для того, чтобы родитель мог получить о нем информацию, - слишком накладно, поэтому используется состояние зомби, в котором ресурсы процесса освобождаются и он возвращается, но его описатель остается. Это временное состояние устанавливается во время вызова sys_exit () (см. более подробную информацию в разделе «Завершение процесса»). Процесс в этом состоянии никогда снова не запустится. Из этого состояния он может перейти только в состояние TASK_STOPPED. Если задача остается в этом состоянии слишком долго, родительский процесс не убивает своих детей. Задача зомби не может быть убита, так как она уже не является живой. Завершение процесса Это значит, что задач для убиения не существует, а существуют только описатели, ожидающие освобождения. TASK_RUNNING в TASKJTOPPED Этот переход выполняется в двух случаях. Первый случай - это когда отладчик или утилита трассировки манипулирует процессом. Второй случай - это когда процесс получает SIGSTOP или один из сигналов на остановку. TASKJJNINTERRUPTIBLE или TASKJNTERRUPTIBLE в TASKJSTOPPED TASK_STOPPED управляет процессами в SMP-системах или в течение обработки сигнала. Процесс устанавливается в состояние TASK_STOPPED, когда процесс получает сигнал на пробуждение или если ядру необходимо, чтобы именно этот процесс не отвечал ни на какие сигналы (как будто он установлен, например, в TASK_INTERRUPTIBLE). Если задача не находится в состоянии TASK_ZOMBIE, процесс устанавливается в состояние TASK_STOPPED до того, как он получит сигнал SIGKILL. Состояние блокировки в состояние готовности Переход из блокированного состояния в состояние готовности происходит после получения данных или доступа к оборудованию, которого ожидает процесс. Два специфичных для Linux перехода, подпадающие под эту категорию, - это из TASKJNTERRUPTIBLE В TASK_RUNNING и ИЗ TASK_INTERRUPTIBLE в TASK_RUNNING. Завершение процесса Процесс может завершаться добровольно явным образом и добровольно неявным образом либо принудительно. Добровольное завершение может быть выполнено двумя способами: 1. В результате возврата из функции main () (неявное завершение). 2. С помощью вызова exit () (явное завершение). Выполнение возвращения из функции main () преобразуется в вызов exit (). При этом компоновщик вставляет вызов exit (). Принудительное завершение может быть получено несколькими способами: 1. Процесс получает сигнал, который не может обработать. 2. Во время выполнения в режиме ядра происходит исключение. 3. Программа получает SIGABRT или другой сигнал на завершение. Завершение процесса обрабатывается различным образом в зависимости от того, ж^ его родитель или нет. Процесс может: • завершиться до родителя, Глава 3 • Процессы: принципиальная модель выполнения • завершиться после родителя. В первом случае дети превращаются в зомби-процессы до того момента, как родитель сделает вызов wait () /waitpid(). Во втором случае статус родителя дочернего процесса будет наследоваться от процесса init(). Таким образом, при завершении процесса ядро проверяет все активные процессы на предмет того, что завершаемый процесс является их родителем. Если такие процессы были найдены, то PID их родителя устанавливается в I1. Посмотрим на пример еще раз и проследим его до самого его конца. Процесс явно вызывает exit (0). (Обратите внимание, что, кроме этого, может быть вызван _exit (), return (0) или программа просто дойдет до конца main без всяких дополнительных вызовов.) Библиотечная С-функция exit () выполняет, в свою очередь, системный вызов sys_exit (). Мы можем просмотреть следующий код и увидим, что происходит с процессом далее. Теперь мы посмотрим функцию, завершающую процесс. Как говорилось ранее, наш процесс f оо вызывает exit (), который вызывает первую рассматриваемую нами функцию - sys_exit(). Мы проследим вызов sys_exit() и углубимся в детали do_exit (). 3.5.1 Функция sys_exit() kernel/exit.с asmlinkage long sys_exit(int error_code) { do_exit( (error_code& 0xff )«8); } sys_exit () для различных архитектур не различается, а их работа довольно понятна -все они выполняют вызов do_exit () и преобразуют код выхода в формат, требуемый ядром. 3.5.2 Функция do_exit() kernel/exit.с 7 07 NORET_TYPE void do_exit(long code) 708 ( 709 struct ta? k_struct *tsk = current; 711 if (unlikely (in_interrupt())) 1 To есть их родителем становится процесс init (). Примеч. науч. ред. Завершение процесса 712 panic(" Aiee, killing interrupt handler! " ); 713 if (unlikely(! tsk-> pid)) 714 panic(" Attempted to kill the idle task! " ); 715 if (unlikely(tsk-> pid == 1)) 716 panic(" Attempted to kill init! " ); 717 if (tsk-> io_context) 718 exit_io_context(); 719 tsk-> flags |= PF_EXITING; 720 del__timer_sync (& tsk-> real__timer); 721 722 if (unlikely(in_atomic())) 723 printk(KERN_INFO " note: %s[%d] exited with preempt_count %d\n", 724 current-> comm, current-> pid, 725 preempt_count()); Строка 707 Код параметра представляет собой код выхода, который процесс возвращает родителю. Строки 711-716 Проверка маловероятных, но возможных непредвиденных ситуаций. Включает следующее: 1. Проверку, что мы не внутри обработчика прерывания. 2. Проверку, что мы не в задаче idle (PID=0) или в задаче init (PID=l). Обратите внимание, что процесс init убивается только при завершении работы системы. Строка 719 Здесь мы устанавливаем PF_EXITING в поле flags структуры задачи. Это означает, что процесс завершается. Например, такая конструкция используется при создании временного интервала для заданного процесса. Флаги процесса проверяются, и тем самым достигается экономия процессорного времени. kernel/exit.с 727 profile_exit__task(tsk); 729 if (unlikely(current-> ptrace & PT_TRACE_EXIT)) { 73 0 current-> ptrace_message = code; 731 ptrace_notify((PTRACE_EVENT_EXIT « 8) | SIGTRAP); 732 } 734 acct_process(code); Глава 3 • Процессы: принципиальная модель выполнения 735 exit_mm(tsk); 737 exit_sem(tsk); 73 8 exit_f iles (tsk); 739 exit_fs(tsk); 740 exit__namespace(tsk); 741 'exit_thread(); Строки 729-732 Если процесс отслеживается и установлен флаг PT_TRACE_EXIT, мы передаем код выхода и уведомляем об этом родительский процесс. Строки 735-742 Эти строки выполняют очистку и перераспределение ресурсов, используемых за память и освобождает структуру mm_struct, ассоциированную с процессом; exit_sem() убирает связь задачи с любыми семафорами IPC; ____ exit_f iles () освобождает любые файлы, используемые процессом, и декрементирует счетчик kernel/exit.с 744 if (tsk-> leader) 745 disassociate_ctty(l); 747 module_put(tsk-> thread_info-> exec_domain-> module); 748 if (tsk-> binfmt) 749 module_put(tsk-> binfmt-> module); Строки 744-475 Если процесс является лидером сессии, можно ожидать, что он имеет контрольный терминал или tty. Эта функция убирает связь между задачей-лидером и контролирующим tty. Строки 747-749 В этом блоке мы уменьшаем счетчик ссылок для модуля: kernel/exit.с 751 tsk-> exit_code = code; Завершение процесса 752 exit_notify(tsk); 754 if (tsk-> exit_signal == -1 & & tsk-> ptrace == 0) 755 release_task(tsk); 757 schedule(); 758 BUGO; 759 /* Избегание " noreturn function does return". */ 760 for(;; ); 761 } Строка 751 Устанавливает код выхода в поле exit_code структуры task_struct. Строка 752 Родителю посылается сигнал SIGCHLD, а состояние задачи устанавливается в TASK_Z0MBIE; exit_notify() уведомляет всех, кто связан с задачей, о ее приближающейся смерти. Родитель информируется о коде выхода, а в качестве родителя детей процесса назначается процесс init. Единственным исключением из этого правила является ситуация, когда другой существующий процесс выходит из той же группы процессов: в этом случае существующий процесс используется как суррогатный родитель. Строка 754 Если exit_signal равен -1 (что означает ошибку) и процесс не является ptraced, ядро вызывает планировщик для освобождения описателя процесса этой задачи и для освобождения его временного среза. Строка 757 Передача процессора новому процессу. Как мы увидим в гл. 7, вызов schedule () не возвращается. Весь код после этой строки обрабатывает неправильные ситуации или избегает замечаний компилятора. 3.5.3 Уведомление родителя и sys_wait4() Когда процесс завершается, об этом уведомляется его родитель. Перед этим процесс находите^ состоянии зомби, когда все ресурсы возвращаются в ядро, и остается только описатель процесса. Родительская задача (например, оболочка Bash) получает сигнал SIGCHLD, посылаемый ядром, когда дочерний процесс завершается. В примере оболочка вызывает wait (), когда хочет получать уведомления. Родительский процесс может игнорировать сигнал, не реализуя обработчик прерывания, и может вместо этого выбрать вызов wait () [или waitpid () ] в любой точке. Глава 3 • Процессы: принципиальная модель выполнения Семейство функций wait служит для решения двух основных задач: • Гробовщик. Получение информации о смерти задачи. • Гробокопатель. Избавление ото всех отслеживаемых процессов. Наша родительская программа может выбирать вызов одной из четырех функций в семействе wait: • pid_t wait(int *status) • pid_t waitpid(pid_t pid, int *status, int options) • pid_t wait3(int *status, int options, struct rusage *rusage) • pid__t wait4 (pid_t pid, int *status, int options, struct rusage *rusage) Каждая функция, в свою очередь, вызывает sys_wait4 (), который порождает множество уведомлений. Процесс, вызывающий функцию wait, блокируется до того, как один из его дочерних процессов завершается или возвращается сразу, если дочерний процесс уже завершен (или если у процесса нет дочерних процессов). Функция sys_wait4 () показывает нам, как ядро управляет этим уведомлением: kernel/exit.с 1031 asmlinkage long sys__wait4 (pid_t pid, unsigned int * stat_addr, int options, struct rusage * ru) 1032 (
1033 DECLARE_WAITQUEUE(wait, current); 1034 struct task_struct *tsk; Int flag, retval; 1036 1037 if (options & ~(WNOHANG|WUNTRACED|___ WNOTHREAD|___ WCLONE |___ WALL) ) 1038 return -EINVAL; 1040 add_wait_queue(& current-> wait_chldexit, & wait); 1041 repeat: 1042 flag = 0; 1043 current-> state = TASK__INTERRUPTIBLE; 1044 read__lock(& tasklist_lock); Завершение процесса Строка 1031 Параметры включают РШ целевого процесса, адрес, куда помещается статус выхода дочернего процесса, флаги для sys_wait4 () и адрес, по которому размещена информация об используемых ресурсах. Строки 1033 и 1040 Определение очереди ожидания и добавление в нее процесса. (Более подробно это описано в разделе «Очередь ожидания».) Строки 1037-1038 Этот код в основном проверяет ошибочные состояния. Функция возвращает код ошибки, если в системный вызов переданы неправильные параметры. В этом случае возвращается ошибка EINVAL. Строка 1042 Переменная flag устанавливается в начальное значение 0. Эта переменная изменяется, как только аргумент pid оказывается принадлежащим к одной из дочерних задач вызова. Строка 1043 Это код, в котором вызывающий код блокируется. Состояние задачи изменяется С TASK_RUNNING на TASK_INTERRUPTIBLE. kernel/exit.с 1045 tsk = current; 1046 do {
1047 struct task_struct *p; 1048 struct list_head *_p; 1049 int ret; 1050 1051 list_for_each(_p, & tsk-> children) { 1052 p = list_entry(_p, struct task_struct, sibling); 1054 ret = eligible_child(pid/ options, p); 1055 if (lret) 1056 continue; 1057 flag = 1; 1058 switch (p-> state) { 1059 case TASK_STOPPED: 1060 if (! (options & WUNTRACED) & & 1061! (p-> ptrace & PT_PTRACED)) 1062 continue; 1063 retval = wait_task_stopped(p, ret == 2, Глава 3 • Процессы: принципиальная модель выполнения 1064 stat_addr, ru); 1065 if (retval! = 0) /* Освобождает блокировку. */ 1066 goto end_wait4; 1067 break; 1068 case TASK_ZOMBIE: 1072 if (ret == 2) 1073 continue; 1074 retval = wait_task__zombie(p, stat_addr, ru); 1075 if (retval! = 0) /* Освобождает блокировку. */ 1076 goto end_wait4; 1077 break; 1078 } 1079 } 1091 tsk = next_thread(tsk); 1092 if (tsk-> signal! = current-> signal) 1093 BUG(); 1094 } while (tsk! = current); Строки 1046 и 1094 Цикл do while выполняется один раз за цикл при поиске себя и затем продолжается при поиске других задач. Строка 1051 Повтор действия для каждого процесса в списке детей задачи. Помните, что при этом родительский процесс ожидает завершения детей. Процесс, находящийся в состоянии TASK__INTERRUPTIBLE, перебирает весь список своих детей. Строка 1054 Определение, имеет ли передаваемый параметр pid допустимое значение. Строки 1058-1079 Проверка состояния каждой дочерней задачи. Действия выполняются, только если ребенок остановлен или в состоянии зомби. Если задача спит, готова или выполняется (предыдущее состояние), ничего не делается. Если дочерний процесс находится в состоянии TASK_STOPPED и используется опция UNTRACED (что означает, что задача не останавливается по причине отслеживания процесса), мы проверяем состояние дочернего процесса, о котором получена информация, и возвращаем информацию об этом дочернем процессе. Если дочерний процесс находится в состоянии TASK_ZOMBIE, он убирается. 3.6 Слежение за процессом: базовые конструкции планировщика kernel/exit.с 1106 retval = -ECHILD; 1107 end_wait4: 1108 current-> state = TASK_RUNNING; 1109 remove_wait_queue(& current-> wait_chldexit, & wait); 1110 return retval; 1111 } Строка 1106 Если мы добрались до этой точки, переданный параметр PID не является дочерним процессом вызывающего процесса. ECHILD - это ошибка, используемая для уведомления об этой ошибке. Строки 1107-1111 В этой точке весь список дочерних процессов обработан и все дочерние процессы, которые нужно было удалить, удалены. Блокировка родителя снимается, и его состояние опять устанавливается в TASK_RUNNING. Наконец, удаляется очередь ожидания. В этой точке вам должны быть знакомы различные состояния, в которых процесс может побывать на протяжении своего жизненного цикла, реализующие их функции ядра и структуры, которые ядро использует для отслеживания всей этой информации. Теперь мы рассмотрим, как планировщик манипулирует и управляет процессами для создания эффекта многопоточной системы. Также мы увидим подробности перехода процесса из одного состояния в другое. 3.6 Слежение за процессом: базовые конструкции планировщика До этого места мы говорили о концепции состояний и переходов между состояниями процессов с позиции процессов. Мы еще не говорили об управлении переходами и инфраструктуре ядра, выполняющих запуск и остановку процессов. Планировщик обрабатывает все эти подробности. Заканчивая исследование жизненного цикла процесса, мы теперь представим вашему вниманию основы планировщика и того, как он взаимодействует с функцией do_f ork () при создании процесса. Базовая структура Планировщик оперирует структурой, называемой очередью выполнения. В системе присутствует по одной очереди выполнения на каждый процессор. Основой структуры очереди выполнения являются два приоритетно-отсортированных массива. Один из них Глава 3 • Процессы: принципиальная модель выполнения содержит активные задачи, а другой - отработавшие. Обычно активная задача выполняется в течение определенного времени, длиной во временной срез или квант времени, а затем вставляется в массив отработавших задач, где ожидает следующей порции процессорного времени. Когда активный массив становится пустым, планировщик меняет местами эти два массива, меняя активный и отработанный указатели. Далее планировщик начинает выполнять задачи из активного массива. Рис. 3.12 иллюстрирует массив приоритетов в очереди ожидания. Структура массива приоритетов определена следующим образом: kernel/sched.с 192 struct prio__array { 193 int nr_active; 194 unsigned long bitmap[BITMAP_SIZE]; 195 struct list_head queue [MAX__PRIO]; 196 }; Структура prio_array имеет следующие поля: • nr_active. Счетчик, хранящий количество задач, находящихся в массиве приоритетов. • bitmap. Следит за приоритетами в массиве. Настоящая длина bitmap зависит от размера unsigned long в системе. Ее всегда достаточно для хранения MAX_PRIO бит, но может быть и больше. • queue. Массив, который хранит список задач. Каждый список хранит задачи с определенными приоритетом. Поэтому queue [ 0 ] хранит список всех задач с приоритетом 0, queue [ 1 ] хранит список всех задач с приоритетом 1 и т. д. С этим базовым пониманием организации очереди выполнения мы можем проследить работу планировщика с одной задачей на однопроцессорной системе. Популярное:
|
Последнее изменение этой страницы: 2016-03-25; Просмотров: 953; Нарушение авторского права страницы