Изучите алгоритмы работы системных вызовов TCP

Алгоритмы работы системных вызовов TCP: от уровня ядра к уровню приложений

Для эффективной работы со стеком TCP/IP вам доступен богатый выбор системных вызовов. Реализация TCP-стека сложна, поэтому для понимания принципов его работы предлагается проследить работу системных вызовов вплоть до уровня ядра. Данная статья будет незаменима для детального изучения алгоритма работы системных вызовов TCP-стека. Описано, какие функции вызываются внутри TCP-стека при обращении к нему с уровня приложений, а также затрагивается TCP-стек FreeBSD.

Бинду Анупама, штатный инженер программного обеспечения, IBM  

Фото of Анупамы БиндуАнупама обладает более чем пятилетним опытом работы в области сетей. Свою профессиональную карьеру она начала в Siemens Communications Systems. В IBM она перешла в апреле 2003 года, где приступила к работе над поддержкой сети и 3-го уровня TCP/IP-стека в OS/2® в должности главного разработчика. С Анупамой вы можете связаться по email anubindu@in.ibm.com.



17.11.2009

Введение

Обычно клиент-серверное приложение, работающее по протоколу TCP, производит серию системных вызовов к TCP-подсистеме для достижения различных целей. Среди таких системных вызовов отметим socket(), bind(), listen(), accept(), send() и receive(). В статье рассказывается, что происходит на нижних уровнях в тот момент, когда приложение делает последовательность системных вызовов, показанных ниже на рисунке 1.

Рисунок 1. Стандартные последовательности обращений приложения к TCP
Стандартные последовательности обращений приложения к TCP

На рисунке 2 представлены различные логические уровни, через которые проходит системный вызов TCP, прожде чем достичь непосредственно физического соединения.

Рисунок 2. Уровни прохождения системного вызова TCP
Уровни прохождения системного вызова TCP

Любой системный вызов TCP прежде достигает уровня сокетов, где проверяется правильность параметров, переданных приложением. Уровень сокетов является протоколо-независимым, так как здесь не анализируется конкретный протокол.

Ниже располагается уровень протокола, содержащий непосредственно реализацию протокола (в нашем случае – TCP). Во время обращения уровня сокетов к уровню протокола проверяется, что структуры данных, общие для обоих уровней, находятся в монопольном доступе. Это нужно для предотвращения повреждения данных.

На уровне интерфейсов работают различные драйверы сетевых устройств, отправляющие и получающие данные из физической сети.

На каждый сокет существует очередь, называемая очередью сокета, на каждый сетевой интерфейс также существует очередь для обмена данными. Однако на уровне протокола имеется единственная очередь, которая называется входной IP-очередью. Данные от уровня интерфейсов поступают на вход этой очереди, а уровень протокола отправляет данные в соответствующие очереди уровня интерфейсов.

В этой статье будут рассмотрены следующие системные вызовы:

Socket

socket (struct proc *p, struct socket_args *uap, int retval)
struct sock_args 
{
int domain, 
int type,
int protocol;
};

Параметры вызова socket следующие:

  • p – указатель на структуру proc, соответствующую процессу, делающему вызов socket.
  • uap – указатель на структуру socket_args, содержащую аргументы, передаваемые системному вызову.
  • retval содержит код возврата системного вызова.

Вызов socket создает новый сокет и присваивает ему дескриптор, который возвращается в вызывающую программу. Последующие обращения используют этот дескриптор. Вызов socket также связывает указанный протокол с созданным дескриптором.

В полях domain, type и protocol передается соответственно семейство, тип и протокол для создаваемого сокета. На рисунке 3 показан алгоритм работы вызова socket.

Рисунок 3. Алгоритм работы socket
Алгоритм работы socket

Приняв параметры от вызывающего процесса, socket вызывает функцию socreate(). socreate(), основываясь на переданных аргументах, находит указатель на структуру селектора протокола protsw, а затем выделяет память под новую структуру для сокета. Далее socreate() вызывает функцию pr_usrreq(), которая, в зависимости от конкретного протокола, производит те или иные действия. Прототип функции pr_usrreq() выглядит так:

int  pr_usrreq(struct socket *so , int req, struct mbuf  *m0 , *m1 , *m2);

Параметры означают следующее:

  • so – указатель на структуру socket.
  • req – тип запроса. В данном случае он равен PRU_ATTACH.
  • m0, m1 и m2 – указатели на структуры типа mbuf, содержимое которых зависит от запроса.

Функция pr_usrreq обрабатывет около 16 видов запросов.

Если указан протокол TCP, то функция tcp_usrreq() вызывает tcp_attach( ), отвечающую за обработку запроса PRU_ATTACH. Для создания управляющей структуры протокола семейства Интернет (Internet protocol control block) вызывается функция in_pcballoc(), которая выделяет необходимую память ядра и производит инициализацию структуры. После этого управление возвращается к tcp_attach().

Управляющая структура протокола TCP (TCP control block) создается функцией tcp_newtcpcb(), которая инициализирует все необходимые поля, включая переменные таймеров TCP, после чего идет возврат в tcp_attach(). Состояние сокета устанавливается в CLOSED. По возврату в tcp_usrreq() дескриптор сокета указывает на управляющую структуру TCP.

Управляющие структуры Интернет-протокола хранятся в виде двусвязного кольцевого списока. Каждая такая структура содержит указатель на структуру socket (чье поле so_pcb указывает на управляющую структуру Интернет-протокола) и указатель на управляющую структуру TCP. Для подробной информации об управляющих структурах Интернет- и TCP-протоколов обратитесь к разделу Ресурсы.

Bind

bind (struct proc *p, struct bind_args *uap, int *retval)
   struct bind_args 
   {   int s;
       caddr_t name;
       int namelen;
   };

Вызов bind принимает следующие параметры:

  • s – дескриптор сокета.
  • name – указатель на буфер, содержащий сетевой адрес транпортного уровня.
  • namelen – размер этого буфера.

Системный вызов bind связывает локальный сетевой адрес транпортного уровня с сокетом. На стороне клиента этот вызов делать не обязательно – при вызове connect ядро автоматически сделает необходимое связывание. На стороне сервера обычно явно вызывают bind перед тем, как начать принимать соединения или производить обмен данными.

bind копирует указанный процессом локальный адрес в буфер mbuf, а затем вызывает функцию sobind, которая, в свою очередь, вызывает tcp_usrreq() с типом запроса PRU_BIND. Оператор ветвления switch в функции tcp_usrreq() вызывает in_pcbbind() – последняя связывает сокет с локальным адресом и портом. При этом функция in_pcbbind() предварительно проверяет, не является ли данный вызов bind повторным для сокета, а также что существует хотя бы один интерфейс с назначенным IP-адресом. Эта функция выполняет всю необходимую работу как по явному, так и неявному связыванию.

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

Рисунок 4. Алгоритм работы системного вызова bind
Алгоритм работы системного вызова bind

Если был указан ненулевой порт, то проверяются права доступа – производить bind для привилегированного порта (т.е. порта с номером < 1024, как принято Berlkey), имеет право только суперпользователь. Далее вызывается in_pcblookup() для поиска управляющей структуры, содержащей указанный локальный IP-адрес и локальный номер порта. in_pcblookup() проверяет, свободна ли для использования пара адрес-порт. Если второй параметр при вызове in_pcbbind() равен NULL или локальный порт равен 0, то управление передается на код, подбирающий подходящий временный порт (например, порт с номером от 1024 до 5000, согласно Berkley), после чего вызывается in_pcblookup() для проверки, занят нужный порт или нет.

Listen

listen (struct proc *p, struct listen_args *uap, int *retval)
struct listen_args
{ int s;
   int backlog;
};

Параметры системного вызова listen следующие:

  • s – дескриптор сокета.
  • backlog – ограничение на размер очереди соединений для сокета.

Вызов listen сообщает уровню протокола, что сокет готов к принятию новых входящих соединений. Существует ограничение на количество соединений, которые ожидают в очереди, при превышении которого следующие запросы на соединения игнорируются.

listen вызывает функцию solisten(), передавая в нее свои параметры – дескриптор сокета и значение backlog. solisten, в свою очередь, просто вызывает tcp_usrreq() с типом запроса PRU_LISTEN. Ветка, соответствующая PRU_LISTEN внутри оператора switch функции tcp_usrreq(), проверяет, связан ли сокет с портом. Если нет либо порт равен 0, то вызывается in_pcbbind() для связывания, как было описано в разделе о Bind.

Если сокет уже связан с неким ненулевым портом, то состояние сокета сразу устанавливается в LISTEN. В противном случае вызывается in_pcbbind() для проведения неявного связывания (однако такой случай встречается крайне редко – все серверные процессы обычно связываются с заранее известным портом). На рисунке 5 показан алгоритм работы listen.

Рисунок 5. Алгоритм работы системного вызова listen
Алгоритм работы системного вызова listen

Accept

accept(struct proc *p, struct accept_args *uap, int *retval);
 struct  accept_args 
{
	int s;
	caddr_t name;
	int *anamelen;
};

Параметры accept следующие:

  • s – дескриптор сокета.
  • name – выходной параметр, принимающий сетевой адрес транспортного уровня удаленной машины.
  • anamelen – размер буфера name.

Системный вызов accept является блокирующим – он ожидает поступления запроса на соединение. После получения запроса создается сокет, соответствующий новому соединению клиента и сервера, и возвращается его дескриптор. Сокет, для которого был вызван accept, остается в состоянии LISTEN и готов к принятию следующих соединений.

Рисунок 6. Алгоритм работы системного вызова accept
Алгоритм работы системного вызова accept

Вначале accept проверяет на корректность переданные аргументы и начинает ожидать запросов на соединение – функция блокируется внутри while-цикла. При поступлении нового соединения уровень протокола уведомляет об этом пользовательскую программу. Затем проверяется, возникали ли ошибки сокета за время ожидания. Если таковые были, то функция возвращается, а сокет готов принимать из очереди последующие соединения и вызывать для них soaccept(). Внутри soaccept() вызывается tcp_usrreq() с типом запроса PRU_ACCEPT, где оператор switch приводит к вызову функции in_setpeeraddr(), которая считывает IP-адрес и порт удаленной стороны из управляющей структуры протокола и отдает их серверному приложению.

Connect

connect (struct proc *p, struct connect_args *uap, int *retval);
struct connect_args 
{
    int s;
    caddr_t name;
    int namelen;
};

Параметры системного вызова connect следующие:

  • s – дескриптор сокета.
  • name – указатель на буфер, содержащий IP-адрес и порт удаленной стороны.
  • namelen – размер буфера name.

Вызов connect обычно производится на клиентской стороне для установления соединения с сервером. Если клиентская программа не вызвала предварительно bind, то система автоматически сделает неявный bind для локального сокета .

Внутри себя системный вызов connect копирует удаленный адрес (т.е. адрес, к которому производится подключение), переданный программой, в пространство ядра и вызывает функцию soconnect(). После завершения soconnect(), connect входит в режим ожидание до тех пор, пока уровень протокола не уведомит о том, что соединение вошло в состояние ESTABLISHED, либо о том, что произошла ошибка сокета. Функция soconnect() проверяет, находится ли сокет в правильном состоянии, и вызывает pr_usrreq() с типом запроса PRU_CONNECT.

Оператор switch внутри tcp_usrreq() проверяет, связан ли сокет с локальным портом, и если нет, то вызывается in_pcbbind() для проведения неявного связывания. Затем вызывается функция in_pcbconnect(), отвечающая за установление маршрута к узлу назначения, нахождение интерфейса для отправки пакета и проверку того, является ли удаленная сокетная пара (т.е. IP-адрес и номер порта), переданная в connect(), уникальной. Затем in_pcbconnect() записывает удаленный IP-адрес и порт в управляющую структуру Интернет-протокола, после чего управление передается в ветку PRU_CONNECT оператора switch.

Далее tcp_usrreq() вызвает функцию soisconnecting(), которая устанавливает состояние клиентского сокета в SYN_SENT. Затем вызывается tcp_output(), посылающая SYN-пакет в сеть, и выполнение возвращается в функцию connect(), которая ожидает уведомления от уровня протокола – либо о переходе соединения в состояние ESTABLISHED, либо о возникновении ошибки сокета.

Рисунок 7. Алгоритм работы системного вызоыва connect
Алгоритм работы системного вызоыва connect

Трехэтапное рукопожатие TCP

На рисунке 8, рисунке 9 и рисунке 10 показан процесс установления соединения, включающий в себя вызов клиентом connect и сервером accept.

Рисунок 8. Алгоритм отправки и получения SYN-пакета
Алгоритм отправки и получения SYN-пакета

Когда клиент вызывает connect, на уровне протокола вызывается функция tcp_output(), которая отправляет SYN-пакет в сетевой интерфейс. Как видно из рисунка 9, в этот момент происходит возврат из soconnect() в connect, а затем – ожидание. Состояние сокета на стороне клиента становится SYN_SENT. Для отправки пакета в сеть уровень протокола вызывает функцию if_output(), являющуюся специфичной для каждого протокола.

Сетевой интерфейс узла назначения (т.е. сервера) получает SYN-пакет, помещает его в очередь ipintrq и инициирует программное прерывание, что приводит к вызову ipintr(), которая, в свою очередь, вызывает tcp_input(). Функция tcp_input(), выполняемая в контексте прерывания, считывает SYN-пакет из очереди ipintrq, обрабатывает его и сохраняет частично созданное соединение в сокетную очередь незавершенных соединений. Теперь состояние сокета на сервера – SYN_RCVD. Функция tcp_input() устроена таким образом, что после принятия пакета может вызывать tcp_output() для отправки ответного пакет, если это необходимо.

Рисунок 9. Алгоритм отправки и получения пакета SYN ACK
Алгоритм отправки и получения пакета SYN ACK

Сервер, обработав SYN-пакет, посылает при помощи последовательных вызовов tcp_output(), ip_output() и if_output() пакет SYN ACK. Сетевой интерфейс клиента, получив этот пакет, помещает его в очередь ipintrq и инициирует программное прерывание. Аналогично, функция ipintr() извлекает пакет из очереди и передает управление tcp_input(). В этот момент считается, что пакет обработан, и вызывается soisconnected() для прекращения ожидания и выхода из connect. Соединение на стороне клиента считается установленным.

Рисунок 10. Алгоритм отправки и получения ACK-пакета
Алгоритм отправки и получения ACK-пакета

Функция tcp_input() на стороне клиента, обработав пакет SYN ACK, вызывает tcp_output() для отправки ACK-пакета обратно серверу. На стороне сервера ACK-пакет обрабатывается функцией tcp_input(), которая передает управление soisconnected(). Последняя удаляет сокет из сокетной очереди незавершенных соединений и помещает его в очередь завершенных соединений. Далее вызывается Wakeup() для вывода accept из состояния ожидания. Соединение на стороне сервера считается установленным.

Shutdown

shutdown (struct proc *p, struct shutdown_args *uap, int *retval);
Struct shutdown_args
{
	int s;
	int how;
}

Параметры системного вызова shutdown следующие:

  • s – дескриптор сокета.
  • how – число, принимающее значение 0, 1 или 2 и указывающее, какую часть соединения необходимо закрыть (чтения, записи или обе части, соответственно).

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

Рисунок 11. Алгоритм работы системного вызова shutdown
Алгоритм работы системного вызова shutdown

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

Если соединение закрывается на запись, то вызывается функция tcp_usrreq() с типом запроса PRU_SHUTDOWN. Оператор switch, находящийся внутри tcp_usrreq(), в ветке PRU_SHUTDOWN вызывает функцию tcp_usrclosed(), которая устанавливает нужное состояние сокета в зависимости от текущего состояния TCP/IP. Состояния сокета, которые он может принимать в процессе своей "жизни", описываются диаграммой состояний TCP/IP. Если необходимо отправить FIN-пакет, то после возрата из tcp_usrclosed() вызывается функция tcp_output() для отправки пакета в сеть.

Close

soo_close(struct file *fp , struct proc *p);

Параметры системного вызова close следующие:

  • fp – указатель на файловую структуру.
  • p – указатель на структуру proc вызывающего процесса.

Системный вызов close закрывает (или прерывает) все существующие соединения сокета.

Функция soo_close() вызывает so_close(), которая сперва проверяет, является ли закрываемый сокет слушающим (т.е. принимающим входящие соединения). Если так, то обе сокетные очереди проверяются на наличие соединений, ожидающих обработки. Для каждого такого соединения вызывается soabort(), которая, в свою очередь, вызывает tcp_usrreq() с типом запроса PRU_ABORT. Оператор switch передает управление функции tcp_drop(), которая проверяет состояние сокета.

Если состояние сокета SYN_RCVD, то состояние сокета устанавливается в CLOSED и клиенту с помощью tcp_output() посылается RST-пакет, после чего сокет закрывается функцией tcp_close(). Эта функция обновляет три поля структуры, отвечающей за метрику при маршрутизации, и освобождает все ресурсы, используемые сокетом.

Если сокет не является слушающим, то управление передается на soclose() для проверки, существует ли управляющая структура, связанная с данным сокетом. Если нет, то сокет освобождается функцией sofree(). Если да, то вызывается tcp_usrreq() с типом запроса PRU_DETACH для отсоединения сокета от TCP-стека. Ветка PRU_DETACH оператора switch вызывает функцию tcp_disconnect(), которая проверяет, является ли состояние сокета ESTABLISHED. Если нет, то tcp_disconnect() вызывает tcp_close() для деинициализации управляющих структур протокола. Иначе проверяется наличие у сокета опции ждущего режима (linger) и само время ожидания. Если эта опция установлена и время ожидания равно 0, то вызывается tcp_drop(), в противном случае вызывается функция tcp_usrclosed(), которая обновляет состояние сокета и посылает, если нужно, FIN-пакет с помощью tcp_output().

На рисунке 12 показано, какие важнейшие функции вызываются при обращении TCP-приложения к close.

Рисунок 12. Алгоритм работы системного вызова close
Алгоритм работы системного вызова close

Send

sendmsg ( struct proc*p, struct sendmsg_args *uap, int retval);
struct sendmsg_args
{	
   int s;
   caddr_t msg;
   int flags;
};

Параметры системного вызова send следующие:

  • s – дескриптор сокета.
  • msg – указатель на структуру msghdr.
  • flags – управляющая информация.

Существует четыре системных вызова для посылки данных в сеть: write, writev, sendto и sendmsg. В данной статье будет рассмотрен только вызов sendmsg(). Оставшиеся три вызова так или иначе обращаются к функции sosend(). Библиотечные функции send, sendto и sendmsg работают только с дескрипторами сокетов, в то время как write и writev – с любым типом дескрипторов.

Рисунок 13. Алгоритм работы sendmsg
Алгоритм работы sendmsg

Системный вызов sendmsg копирует данные на отправку из пространства пользователя в пространство ядра, а затем вызывает функцию sendit(), где инициализируется специальная структура для чтения данных из пространства процесса в буферы ядра. Наряду с этим копируются адреса и управляющая информация. После этого на арену выходит функция sosend(), отвечающая за следующие действия:

  • Инициализация различных параметров исходя из переданных в sendit() данных.
  • Проверка на правильность состояния сокета и состояния соединения, а также вычисление объема памяти, необходимого для передачи сообщения другим функциям и для возврата ошибок.
  • Выделение памяти и копирование сообщения из пространства процесса.
  • Необходимые действия для отправки данных в сеть, характерные для конкретного протокола.

Далее вызывается tcp_usrreq(), где в зависимости от переданных процессом флагов, управление передается оператором switch либо на ветку PRU_SEND, либо на ветку PRU_SENDOOB (отправка внеполосных данных). В последнем случае, если размер буфера отправки превышает 512 байт, то освобождается вся выделенная до этого память и ветка завершается. Если буфер отправки не превышает 512 байт, то как для PRU_SEND, так и для PRU_SENDOOB вызываются функции sbappend() и tcp_output(), которые добавляют сообщение в конец буфера отправки и отсылают данные в сеть, соответственно.

Receive

recvmsg(struct proc *p, struct recvmsg_args *uap , int *retval);
struct recvmsg_args 
{
 int s,
 struct msghdr *msg,
 int flags,
};

Параметры системного вызова receive следующие:

  • s – дескриптор сокета.
  • msg – указатель на структуру msghdr.
  • flags – управляющая информация.

Существует четыре системных вызова для приема данных: read, readv, recvfrom и recvmsg. Библиотечные фукнции recv, recvfrom и recvmsg работают только с дескрипторами сокетов, в то время как read и readv – с любым типом дескрипторов, которые, применительно к сокетам, вызывают soreceive().

На рисунке 14 представлен алгоритм работы системного вызовы recvmsg. В функциях recvmsg() и recvit() инициализируются различные массивы и структуры, нужные для передачи принятых из сети данных из пространства пользователя в пространство ядра. recvit() вызывает функцию soreceive(), которая копирует полученные из сети данные, находящиеся в буферах сокетов, в приемный буфер приложения. soreceive() производит различные действия, такие как:

  • Проверяет, установлен или нет флаг MSG_OOB.
  • Проверяет, ожидает ли процесс в данный момент получения других данных.
  • Проверяет, нужно ли блокироваться до приема определенного объема данных.
  • Передает полученные данный в пространство пользователя.
  • Проверяет, являются ли принятные данные внеполосными, и предпринимает соответствующие шаги.
  • Оповещает уроверь протокола о завершении приема данных.
Рисунок 14. Алгоритм работы системного вызова recvmsg
Алгоритм работы системного вызова recvmsg

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

Заключение

Прочитав данную статью, вы познакомились с наиболее важными системными вызовами TCP и внутренними функциями ядра, реализующими эти вызовы. Работа системных вызовов TCP детально проиллюстрирована на рисунках. Также статья будет вашим незаменимым помощником при изучении TCP/IP-стека FreeBSD.

Ресурсы

Научиться

Получить продукты и технологии

  • Разработайте ваш следующий проект с помощью пробного ПО от IBM (EN), которое можно загрузить прямо с developerWorks.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=447509
ArticleTitle=Изучите алгоритмы работы системных вызовов TCP
publish-date=11172009