This post is about the urban myth of an interview question that asks:

“What happens when you go to a browser and type in a URL?”

In my opinion, there is no single good answer to that question. If you ask 100 software and hardware engineers, you will get 100 different answers.

Life of a Packet Link to heading

We will start with the network device layer because I don’t want to mess around with user-space HTTPS and sockets tonight. Let’s assume the browser uses HTTP to call send on a socket. Basically, this is what happens(summarized by ChatGPT)

  • A user-space application sends data using BSD socket API syscalls.
  • The Linux kernel networking stack does the following steps:
    • TCP/UDP packing
    • IP packing
    • Ethernet: encapsulates the data into an Ethernet frame and sends it to the Ethernet driver (in this case, e1000)
  • The e1000 driver:
    • Allocates a TX descriptor.
    • Copies packet data into a DMA buffer.
    • Informs the NIC to send the packet.
  • The NIC fetches the packet via DMA and transmits it on the wire.
  • The NIC may raise an interrupt to notify the driver of TX completion.

Below I step through important pieces of that path and add short, plain-language explanations for each snippet.

Socket syscall Link to heading

Starting with call trace from system call send called from C program.

send()/sendmsg()
__sys_sendto()              [net/socket.c]
sock_sendmsg()
__sock_sendmsg()
sock->ops->sendmsg()        // proto_ops table

First, Let’s look at the socket syscall in net/socket.c:

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
	unsigned int, flags)
{
	return __sys_sendto(fd, buff, len, flags, NULL, 0);
}

This is the user-to-kernel entry point. When a program calls send(), it traps into the kernel and eventually calls __sys_sendto. From here the kernel will build a message and hand it to the socket layer.

int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
		 struct sockaddr __user *addr, int addr_len)
{
	...
	return __sock_sendmsg(sock, &msg);
}

__sys_sendto prepares the message and calls __sock_sendmsg which is the generic socket-send implementation. This function can perform checks, copy data from userspace, and pick the correct transport (IPv4/IPv6/TCP/UDP).

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
	int ret = INDIRECT_CALL_INET(READ_ONCE(sock->ops)->sendmsg, inet6_sendmsg,
			     inet_sendmsg, sock, msg,
			     msg_data_left(msg));
	BUG_ON(ret == -EIOCBQUEUED);

	if (trace_sock_send_length_enabled())
		call_trace_sock_send_length(sock->sk, ret, 0);
	return ret;
}

This code routes the send to the appropriate protocol implementation (IPv4 vs IPv6). The INDIRECT_CALL_INET macro hides the protocol differences. The important part: by this point the kernel chooses the right protocol code and continues down the network stack.

net/core/skbuff.c

static int sendmsg_unlocked(struct sock *sk, struct msghdr *msg)
{
	struct socket *sock = sk->sk_socket;

	if (!sock)
		return -EINVAL;
	return sock_sendmsg(sock, msg);
}

sendmsg_unlocked is a thin wrapper that calls sock_sendmsg. Eventually the socket layer will create a sk_buff (socket buffer) that holds the packet data and metadata.

TCP Link to heading

The TCP call stack is something like this:

inet_sendmsg()              [net/ipv4/af_inet.c]
tcp_sendmsg()               [net/ipv4/tcp.c]
tcp_push()                  [net/ipv4/tcp_output.c]
__tcp_push_pending_frames()
tcp_write_xmit()
tcp_transmit_skb()	

starting with inet_sendmsg in `net/ipv4/af_inet.c

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
	struct sock *sk = sock->sk;

	if (unlikely(inet_send_prepare(sk)))
		return -EAGAIN;

	return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg,
			       sk, msg, size);
}

then tcp_sendmsg in net/ipv4/tcp.c

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
	int ret;

	lock_sock(sk);
	ret = tcp_sendmsg_locked(sk, msg, size);
	release_sock(sk);

	return ret;
}

The TCP layer can call IP functions to queue packets. tcp_connect builds TCP headers (e.g., SYN) and then calls into IP to transmit the packet. The TCP code uses helper functions that wrap the IP transmit calls defined in net/ipv4/tcp_output.c

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
				  tcp_sk(sk)->rcv_nxt);
}
/* This routine actually transmits TCP packets queued in by
 * tcp_do_sendmsg().  This is used by both the initial
 * transmission and possible later retransmissions.
 * All SKB's seen here are completely headerless.  It is our
 * job to build the TCP header, and pass the packet down to
 * IP so it can do the same plus pass the packet off to the
 * device.
 *
 * We are working here with either a clone of the original
 * SKB, or a fresh unique copy made by the retransmit engine.
 */
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
			      int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{

	err = INDIRECT_CALL_INET(icsk->icsk_af_ops->queue_xmit,
				 inet6_csk_xmit, ip_queue_xmit,
				 sk, skb, &inet->cork.fl);
INDIRECT_CALLABLE_DECLARE(int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl));
INDIRECT_CALLABLE_DECLARE(int inet6_csk_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl));
INDIRECT_CALLABLE_DECLARE(void tcp_v4_send_check(struct sock *sk, struct sk_buff *skb));

IP Link to heading

from the TCP later, things are passed to IP at ip_queue_xmit and rest of IP layer call stack

tcp_transmit_skb()
ip_queue_xmit()             [net/ipv4/ip_output.c]
dst_output()                // routing dst->output points to ip_output
ip_output()
ip_finish_output()
ip_finish_output2()
neigh_output()              [net/core/neighbour.c]
dev_queue_xmit()            [net/core/dev.c]

net/ipv4/ip_output.c

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
	return __ip_queue_xmit(sk, skb, fl, READ_ONCE(inet_sk(sk)->tos));
}

At this stage the kernel calls into IP’s queue_xmit function. queue_xmit is responsible for constructing IP headers and deciding on routing and how to hand the packet to the next layer (link layer/device). This is the handoff point from the transport layer (TCP) to the network layer (IP).

Ethernet Link to heading

dev_queue_xmit()
qdisc_enqueue()             // enqueue to selected qdisc
qdisc_run()
qdisc_restart()
dev_hard_start_xmit()
dev->netdev_ops->ndo_start_xmit()  // for e1000 → e1000_xmit_frame()

It starts with dev_queue_xmit being called with an sk_buff (see include/linux/netdevice.h):

static inline int dev_queue_xmit(struct sk_buff *skb)
{
	return __dev_queue_xmit(skb, NULL);
}

dev_queue_xmit is the API the IP layer calls to send a packet out on the wire. It takes an sk_buff and finds the correct device (skb->dev) to transmit on.

__dev_queue_xmit is defined in:

  • net/core/dev.c
  • net/ethernet/eth.c
int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{

__dev_queue_xmit does queueing and Qdisc handling (traffic control). It prepares the packet for the device and eventually calls the hard-start transmit function.

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				struct netdev_queue *txq, int *ret)
{
	struct sk_buff *skb = first;
	int rc = NETDEV_TX_OK;

	while (skb) {
		struct sk_buff *next = skb->next;

		skb_mark_not_on_list(skb);
		rc = xmit_one(skb, dev, txq, next != NULL);

dev_hard_start_xmit walks a linked list of skb’s and calls xmit_one for each. This function is the bridge between generic kernel queueing and the device-specific transmit function.

static int xmit_one(struct sk_buff *skb, struct net_device *dev,
			struct netdev_queue *txq, bool more)
{
	unsigned int len;
	int rc;

	if (dev_nit_active(dev))
		dev_queue_xmit_nit(skb, dev);

	len = skb->len;
	trace_net_dev_start_xmit(skb, dev);
	rc = netdev_start_xmit(skb, dev, txq, more);
	trace_net_dev_xmit(skb, rc, dev, len);

	return rc;
}

xmit_one does some bookkeeping and then calls netdev_start_xmit, which will call the device’s ndo_start_xmit implementation (the driver’s transmit function).

netdev_start_xmit is defined in include/linux/netdevice.h:

static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
					struct netdev_queue *txq, bool more)
{
	const struct net_device_ops *ops = dev->netdev_ops;
	netdev_tx_t rc;

	rc = __netdev_start_xmit(ops, skb, dev, more);
	if (rc == NETDEV_TX_OK)
		txq_trans_update(txq);

	return rc;
}
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
					struct sk_buff *skb, struct net_device *dev,
					bool more)
{
	netdev_xmit_set_more(more);
	return ops->ndo_start_xmit(skb, dev);
}

The ops->ndo_start_xmit call is the critical handoff to the network driver. For many drivers this function will map the skb into DMA, fill descriptors in a ring buffer, and then kick the NIC to start transmission.

E1000 Link to heading

At this point the E100 driver takes over to send the ethernet packets through TX queue.

e1000_xmit_frame()          [drivers/net/ethernet/intel/e1000/e1000_main.c]
e1000_tx_queue()
e1000_maybe_stop_tx()

And in drivers/net/ethernet/intel/e1000/e1000_main.c, ndo_start_xmit is initialized to e1000_xmit_frame, which leads to the e1000 NIC driver.

static const struct net_device_ops e1000_netdev_ops = {
	.ndo_open		= e1000_open,
	.ndo_start_xmit		= e1000_xmit_frame,

We’ll also look at the configuration when the interface is brought up with e1000_open.

ndo_open is defined with e1000_netdev_ops in drivers/net/ethernet/intel/e1000/e1000_main.c:

static const struct net_device_ops e1000_netdev_ops = {
	.ndo_open		= e1000_open,
	.ndo_stop		= e1000_close,
	.ndo_start_xmit		= e1000_xmit_frame,

Let’s dig deeper into e1000 methods:

int e1000_open(struct net_device *netdev)
{
	struct e1000_adapter *adapter = netdev_priv(netdev);
	struct e1000_hw *hw = &adapter->hw;
	int err;

	/* Disallow open during test */
	if (test_bit(__E1000_TESTING, &adapter->flags))
		return -EBUSY;

e1000_open configures the device (RX/TX rings, descriptors, interrupts) and prepares it to send/receive packets.

static void e1000_configure(struct e1000_adapter *adapter)
{
	struct net_device *netdev = adapter->netdev;
	int i;

	e1000_set_rx_mode(netdev);
	e1000_restore_vlan(adapter);
	e1000_init_manageability(adapter);
	e1000_configure_tx(adapter);
	e1000_setup_rctl(adapter);
	e1000_configure_rx(adapter);

The driver writes hardware registers that point to descriptor rings and configures filters and offload features. After this, the NIC is ready to accept descriptors from the driver.

static void e1000_configure_tx(struct e1000_adapter *adapter)
{
	u64 tdba;
	struct e1000_hw *hw = &adapter->hw;
	u32 tdlen, tctl, tipg;
	u32 ipgr1, ipgr2;

	/* Setup the HW Tx Head and Tail descriptor pointers */

	switch (adapter->num_tx_queues) {
	case 1:
	default:
		tdba = adapter->tx_ring[0].dma;
		tdlen = adapter->tx_ring[0].count *
			sizeof(struct e1000_tx_desc);
		ew32(TDLEN, tdlen);
		ew32(TDBAH, (tdba >> 32));
		ew32(TDBAL, (tdba & 0x00000000ffffffffULL));
		ew32(TDT, 0);
		ew32(TDH, 0);
		adapter->tx_ring[0].tdh = ((hw->mac_type >= e1000_82543) ?
					   E1000_TDH : E1000_82542_TDH);
		adapter->tx_ring[0].tdt = ((hw->mac_type >= e1000_82543) ?
					   E1000_TDT : E1000_82542_TDT);
		break;
	}

The driver programs the address and length of the TX descriptor ring (a circular buffer). The NIC will read descriptors from this memory area using DMA.

static netdev_tx_t e1000_xmit_frame(struct sk_buff *skb,
				    struct net_device *netdev)
{
	mss = skb_shinfo(skb)->gso_size;
	if (mss) {
		u8 hdr_len;

		/* TSO Workaround for 82571/2/3 Controllers -- if skb->data
		 * points to just header, pull a few bytes of payload from
		 * frags into skb->data
		 */
		hdr_len = skb_tcp_all_headers(skb);
		/* we do this workaround for ES2LAN, but it is unnecessary,
		 * avoiding it could save a lot of cycles
		 */
		if (skb->data_len && (hdr_len == len)) {
			unsigned int pull_size;

			pull_size = min_t(unsigned int, 4, skb->data_len);
			if (!__pskb_pull_tail(skb, pull_size)) {
				e_err("__pskb_pull_tail failed.\n");
				dev_kfree_skb_any(skb);
				return NETDEV_TX_OK;
			}
			len = skb_headlen(skb);
		}
	}

	/* Reserve a descriptor for the offload context */
	if ((mss) || (skb->ip_summed == CHECKSUM_PARTIAL))
		count++;
	count++;

	count += DIV_ROUND_UP(len, adapter->tx_fifo_limit);
	/* Need: count + 2 desc gap to keep tail from touching
	 * head, otherwise try next time
	 */
	if (e1000_maybe_stop_tx(tx_ring, count + 2))
		return NETDEV_TX_BUSY;

	if (skb_vlan_tag_present(skb)) {
		tx_flags |= E1000_TX_FLAGS_VLAN;
		tx_flags |= (skb_vlan_tag_get(skb) <<
		     E1000_TX_FLAGS_VLAN_SHIFT);
	}

	first = tx_ring->next_to_use;

	tso = e1000_tso(tx_ring, skb, protocol);
	if (tso < 0) {
		dev_kfree_skb_any(skb);
		return NETDEV_TX_OK;
	}

	if (tso)
		tx_flags |= E1000_TX_FLAGS_TSO;
	else if (e1000_tx_csum(tx_ring, skb, protocol))
		tx_flags |= E1000_TX_FLAGS_CSUM;

	/* Old method was to assume IPv4 packet by default if TSO was enabled.
	 * 82571 hardware supports TSO capabilities for IPv6 as well...
	 * we can no longer assume that.
	 */
	if (protocol == htons(ETH_P_IP))
		tx_flags |= E1000_TX_FLAGS_IPV4;

	if (unlikely(skb->no_fcs))
		tx_flags |= E1000_TX_FLAGS_NO_FCS;

	/* If count is 0 then mapping error has occurred */
	count = e1000_tx_map(tx_ring, skb, first, adapter->tx_fifo_limit,
		     nr_frags);
	if (count) {
		if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP) &&
		    (adapter->flags & FLAG_HAS_HW_TIMESTAMP)) {
			if (!adapter->tx_hwtstamp_skb) {
				skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
				tx_flags |= E1000_TX_FLAGS_HWTSTAMP;
				adapter->tx_hwtstamp_skb = skb_get(skb);
				adapter->tx_hwtstamp_start = jiffies;
				schedule_work(&adapter->tx_hwtstamp_work);
			} else {
				adapter->tx_hwtstamp_skipped++;
			}
		}

		skb_tx_timestamp(skb);
		netdev_sent_queue(netdev, skb->len);
		e1000_tx_queue(tx_ring, tx_flags, count);
		/* Make sure there is space in the ring for the next send. */
		e1000_maybe_stop_tx(tx_ring,
			    ((MAX_SKB_FRAGS + 1) *
			     DIV_ROUND_UP(PAGE_SIZE,
				  adapter->tx_fifo_limit) + 4));

		if (!netdev_xmit_more() ||
		    netif_xmit_stopped(netdev_get_tx_queue(netdev, 0))) {
			if (adapter->flags2 & FLAG2_PCIM2PCI_ARBITER_WA)
				e1000e_update_tdt_wa(tx_ring,
				     tx_ring->next_to_use);
			else
				writel(tx_ring->next_to_use, tx_ring->tail);
		}
}

e1000_xmit_frame is the driver’s transmit routine. In plain terms:

  • It checks the packet and driver/NIC state.
  • It calculates how many descriptors the packet will need (headers + fragments).
  • It prepares descriptors (via e1000_tx_map) that point to the packet buffers.
  • It updates the NIC tail pointer (writing to a device register) so the NIC knows there are new descriptors to process.

The NIC then reads the descriptors via DMA and transmits the packet on the wire.