{ "cells": [ { "cell_type": "markdown", "id": "dfbf0c9c", "metadata": {}, "source": [ "# Создание процессов #" ] }, { "cell_type": "markdown", "id": "d3a89d3c", "metadata": {}, "source": [ "На этой лекции мы поговорим про создание процессов именно на Python. Мы узнаем, как создать дочерний процесс, поговорим о том, как работает системный вызов `fork`, а также рассмотрим примеры создания процессов при помощи модуля `multiprocessing`.\n", "\n", "Процесс в операционной системе создается при помощи системного вызова `fork`. Давайте рассмотрим программу, которая создает дочерний процесс при помощи системного вызова `fork`. Я ее привел на слайде." ] }, { "cell_type": "code", "execution_count": null, "id": "b8d07b01", "metadata": {}, "outputs": [], "source": [ "# Создание процесса на Python\n", "import time\n", "import os\n", "\n", "pid = os.fork()\n", "\n", "if pid == 0:\n", " # дочерний процесс\n", " while True:\n", " print(f\"child: {os.getpid()}\" )\n", " time.sleep(5)\n", " \n", "else:\n", " # родительский процесс\n", " print(f\"parent: {os.getpid()}\")\n", " os.wait()" ] }, { "cell_type": "markdown", "id": "4bfa662c", "metadata": {}, "source": [ "Импортируем пару модулей `time` и `os`, затем вызываем системный вызов `fork`. Системный вызов `fork` создает точную копию родительского процесса. Это означает, что вся память, все файловые дискрипторы и все ресурсы, которые были доступны в родительском процессе, будут целиком и полностью скопированы в дочернем процессе. То есть после того, как системный вызов `fork` отработал, с этого момента у нас два процесса в операционной системе. Единственное отличие заключается в том, что системный вызов `fork` в родительский процесс вернет `pid` дочернего процесса, а в дочернем процессе переменная `pid` будет равна нулю.\n", "\n", "Код, который я сейчас выделил, будет исполнен в дочернем процессе, а код, который находится за веткой `else`, будет исполнен в родительском процессе. Итак, в родительском процессе мы вызываем системный вызов `os.wait`, это еще один дополнительный системный вызов и он позволяет нам дожидаться завершения дочернего процесса созданного. А в дочернем процессе вна бесконечном цикле выводим `pid` нашего процесса, который создали, и спим пять секунд.\n", "\n", "Давайте попробуем выполнить этот код в консоли и поизучаем снова характеристики наших процессов созданных. Наш пример. Запускаем его." ] }, { "cell_type": "code", "execution_count": 1, "id": "1c7aebc9", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "parent: 19405\n", "child: 19406\n", "child: 19406\n", "^C\n", "Traceback (most recent call last):\n", " File \"ex2.py\", line 15, in \n", "Traceback (most recent call last):\n", " File \"ex2.py\", line 11, in \n", " os.wait()\n", "KeyboardInterrupt\n", " time.sleep(5)\n", "KeyboardInterrupt\n" ] } ], "source": [ "! python ex2.py" ] }, { "cell_type": "markdown", "id": "b139ad9e", "metadata": {}, "source": [ "Итак, создалось два процесса, родительский и дочерний. Давайте попробуем поискать их в операционной системе. Переключимся в другую консоль при помощи известной нам команды `ps uax`. Найдем наш процесс." ] }, { "cell_type": "code", "execution_count": 2, "id": "c17304c7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\n", "mikhayl+ 19407 0.0 0.0 125020 5736 pts/8 S+ 13:53 0:00 python ex2.py\n", "mikhayl+ 19408 0.0 0.0 125020 3632 pts/8 S+ 13:53 0:00 python ex2.py\n", "mikhayl+ 19409 0.0 0.0 113128 1300 pts/9 Ss+ 13:56 0:00 /bin/bash -c ps aux | head -1; ps aux | grep ex2.py\n", "mikhayl+ 19413 0.0 0.0 112660 920 pts/9 S+ 13:56 0:00 grep ex2.py\n" ] } ], "source": [ "! ps aux | head -1; ps aux | grep ex2.py" ] }, { "cell_type": "markdown", "id": "6c806b8c", "metadata": {}, "source": [ "Итак, мы видим процесс c `pid`'ом 19407, 19408. Это и есть наши созданные процессы. Можно отобразить результаты команды `ps` в иерархическом виде, чтобы посмотреть, какой из процессов родительский, а какой дочерний. Делается это при помощи дополнительного флага `f`." ] }, { "cell_type": "code", "execution_count": 3, "id": "a465721e", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\n", "mikhayl+ 19414 0.0 0.0 113128 1304 pts/9 Ss+ 13:56 0:00 | | \\_ /bin/bash -c ps auxf | head -1; ps auxf | grep ex2.py\n", "mikhayl+ 19418 0.0 0.0 112660 924 pts/9 S+ 13:56 0:00 | | \\_ grep ex2.py\n", "mikhayl+ 19407 0.0 0.0 125020 5736 pts/8 S+ 13:53 0:00 | \\_ python ex2.py\n", "mikhayl+ 19408 0.0 0.0 125020 3632 pts/8 S+ 13:53 0:00 | \\_ python ex2.py\n" ] } ], "source": [ "! ps auxf | head -1; ps auxf | grep ex2.py" ] }, { "cell_type": "markdown", "id": "d2b2c77e", "metadata": {}, "source": [ "Действительно видим наш родительский процесс и дочерний. Давайте посмотрим, что процессы делают. Для этого нам понадобится команда `strace` и дополнительные права." ] }, { "cell_type": "code", "execution_count": 4, "id": "66c5163f", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "strace: Process 19408 attached\n", "select(0, NULL, NULL, NULL, {0, 365190}) = 0 (Timeout)\n", "write(1, \"child: 19408\\n\", 13) = 13\n", "select(0, NULL, NULL, NULL, {5, 0}^C\n", "strace: Process 19408 detached\n", " \n" ] } ], "source": [ "! strace -p 19408" ] }, { "cell_type": "markdown", "id": "207d0457", "metadata": {}, "source": [ "Итак, мы видим, что наш созданный дочерний процесс делает системный вызов `write` и выводит информацию в стандартный поток вывода. Теперь узнаем, что делает родительский процесс." ] }, { "cell_type": "code", "execution_count": 5, "id": "f95d20b7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "strace: Process 19407 attached\n", "wait4(-1, ^C\n", "strace: Process 19407 detached\n", " \n" ] } ], "source": [ "! strace -p 19407" ] }, { "cell_type": "markdown", "id": "25bd9db4", "metadata": {}, "source": [ "Родительский процесс сделал системный вызов `wait`, и операционная система сама оповестит его о том, когда дочерний процесс завершится.\n", "\n", "Давайте остановимся немного еще раз на памяти в родительском и дочернем процессе и рассмотрим пример. Итак, у нас есть программа, мы объявили в ней переменную `foo`, присвоили ей значение \"bar\" и делаем системный вызов `fork`." ] }, { "cell_type": "code", "execution_count": null, "id": "b7175183", "metadata": {}, "outputs": [], "source": [ "# Память родительского и дочернего процесса\n", "import os\n", "\n", "foo = \"bar\"\n", "\n", "if os.fork() == 0:\n", " # дочерний процесс\n", " foo = \"baz\"\n", " print(\"child:\", foo)\n", " \n", "else:\n", " # родительский процесс\n", " print(\"parent:\", foo)\n", " os.wait()" ] }, { "cell_type": "markdown", "id": "dbec21e8", "metadata": {}, "source": [ "Вот этот код мы исполняем в дочернем процессе. После того, как отработал системный вызов `fork`, как я уже говорил, вся память целиком и полностью будет скопирована из родительского процесса в дочерний. То есть у нас переменная `foo` будет точно так же доступна в дочернем процессе. И у нее будет значение \"bar\", так как память была скопирована. Но если мы изменим значение `foo` в дочернем процессе, это никак не повлияет на родительский процесс, на переменную `foo`, которая была объявлена в родительском процессе. Если мы выведем в родительском процессе ее значение, то оно будет равно значению \"bar\"." ] }, { "cell_type": "code", "execution_count": 6, "id": "fef004b6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "parent: bar\r\n", "child: baz\r\n" ] } ], "source": [ "! python ex3.py" ] }, { "cell_type": "markdown", "id": "280c49e6", "metadata": {}, "source": [ "Итак, память целиком и полностью копируется. Если ее рассматривать, то это разные памяти. У дочернего процесса своя, у родительского процесса своя. То же самое относится и к файловым дискрипторам.\n", "\n", "Рассмотрим пример, который я привел на слайде." ] }, { "cell_type": "code", "execution_count": null, "id": "36018536", "metadata": {}, "outputs": [], "source": [ "# Файлы в родительском и дочернем процессе\n", "# $ cat data.txt\n", "# example string1\n", "# example string2\n", "\n", "import os\n", "f = open(\"data.txt\")\n", "foo = f.readline()\n", "\n", "if os.fork() == 0:\n", " # дочерний процесс\n", " foo = f.readline()\n", " print(f\"child: {foo}\")\n", " \n", "else:\n", " # родительский процесс\n", " foo = f.readline()\n", " print(f\"parent: {foo}\")" ] }, { "cell_type": "markdown", "id": "9cbbb1a0", "metadata": {}, "source": [ "Предположим, у нас есть небольшой файл. Я подготовил такой файл с двумя строчками `example string1` и `example string2`. Тоже так же импортируем системный модуль `os`. Открываем файл на чтение и читаем в переменную `foo` одну строчку. После того, как мы считали одну строчку, делаем системный вызов `fork`. После этого у нас создается точная копия родительского процесса и все файловые дискрипторы, вся память опять Если мы в дочернем процессе снова вызовем метод `readline` у файла или у объекта `f`, то мы прочитаем уже вторую строчку из этого файла. И снова это никак не повлияет на родительский процесс. В родительском процессе, если мы вызовем `readline`, то мы точно так же считаем вторую строчку.\n", "\n", "Итак, еще раз обращаю внимание, что не только память, но и файловые дискрипторы целиком и полностью копируются в дочернем процессе, когда мы делаем системный вызов `fork`." ] }, { "cell_type": "code", "execution_count": 7, "id": "5407785f", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "parent: example string2\r\n", "child: example string2\r\n" ] } ], "source": [ "! python ex4.py" ] }, { "cell_type": "markdown", "id": "e4464c2d", "metadata": {}, "source": [ "Все эти примеры носят обучающий характер, и обычно код с использованием системных вызовов `fork` немного сложнее. `fork` может вернуть ошибку, ее нужно проверять, и обычно в Python'е используют модуль `multiprocessing` для создания процессов. Рассмотрим пример." ] }, { "cell_type": "code", "execution_count": null, "id": "2d94def7", "metadata": {}, "outputs": [], "source": [ "# Создание процесса, модуль multiprocessing\n", "from multiprocessing import Process\n", "\n", "def f(name):\n", " print(f\"hello {name}\")\n", " \n", "p = Process(target=f, args=(\"Bob\",))\n", "p.start()\n", "p.join()" ] }, { "cell_type": "markdown", "id": "d3b5b21d", "metadata": {}, "source": [ "Для того, чтобы запустить процесс на Python необходимо импортировать класс `Process` из модуля `multiprocessing`, создать объект класса `Process`, передать ему в конструктор функцию, которую мы хотим исполнить в отдельном дочернем процессе и аргументы этой функции. После того, как мы создали объект, никакого процесса создано не будет. Процесс будет создан тогда, когда мы вызовем метод `start` нашего объекта. Вот здесь, внутри метода `start`, будет вызван системный вызов `fork` и исполнена наша функция `f` в отдельном процессе. Очень важно ожидать завершения всех созданных дочерних процессов. Для этого можно воспользоваться удобной функцией `join` для нашего объекта `p`. Как мы видим, системные вызовы `fork` и `wait` спрятаны внутри красивых оберток. Вообще, не в каждой операционной системе есть системный вызов `fork` и поэтому в `multiprocessing` все аккуратно сделано за вас. И не надо за это беспокоиться. Просто используем модуль `multiprocessing` для создания собственных дочерних процессов. Если мы исполним этот пример, то в стандартном выводе мы увидим строчку `hello Bob`." ] }, { "cell_type": "code", "execution_count": 8, "id": "d5a38c40", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello Bob\r\n" ] } ], "source": [ "! python ex5.py" ] }, { "cell_type": "markdown", "id": "b27125c7", "metadata": {}, "source": [ "Существует также альтернативный метод создания процесса при помощи `multiprocessing` – при помощи наследования." ] }, { "cell_type": "code", "execution_count": null, "id": "222e55c3", "metadata": {}, "outputs": [], "source": [ "# Создание процесса, модуль multiprocessing\n", "from multiprocessing import Process\n", "\n", "class PrintProcess(Process):\n", " def __init__(self, name):\n", " super().__init__()\n", " self.name = name\n", "\n", " def run(self):\n", " print(f\"hello {self.name}\")\n", " \n", "p = PrintProcess(\"Mike\")\n", "p.start()\n", "p.join()" ] }, { "cell_type": "markdown", "id": "48c2df7d", "metadata": {}, "source": [ "Для этого мы объявляем свой класс, наследуемся от класса `multiprocessing Process`. В конструктор передаем нужные параметры для функции, которая должна быть запущена в дочернем процессе. И переопределяем метод `run`. В методе `run` мы реализуем код, вызываем функции дополнительные и используем все параметры. Дальше все стандартно. Создаем объект нашего класса `PrintProcess`, передаем туда параметры, вызываем метод `start`. Метод `start` вызовет `fork` и выполнит наш код в дочернем процессе. Для завершения дочернего процесса мы вызываем метод `join`. Если мы выполним этот код, то увидим вывод `hello Mike`." ] }, { "cell_type": "code", "execution_count": 9, "id": "1be7cfff", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello Mike\r\n" ] } ], "source": [ "! python ex6.py" ] }, { "cell_type": "markdown", "id": "b7a2cbd1", "metadata": {}, "source": [ "Очень важно ожидать завершения всех дочерних процессов, чтобы контролировать освобождение всех ресурсов.\n", "\n", "Итак, на этой лекции мы поговорили о том, как создать дочерний процесс, обсудили детали работы системного вызова `fork`, а также обсудили, как создать процесс при помощи удобных методов из модуля `multiprocessing`. Мы не говорили о вопросах обмена данными между процессами и синхронизации процессов. Мы рассмотрим эти вопросы на примере потоков." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }