Back to Blog
By AriesZhou · · 27 min read

A Comprehensive Guide to Linux Inter-Process Communication (IPC)

System

IPC (Inter-Process Communication) is the mechanism for data exchange between different processes in a Linux system. It mainly includes:

  • Pipe: Primarily used for parent-child process communication, divided into anonymous pipes and named pipes (FIFO).
  • Signal: An asynchronous communication method used to notify the receiving process that an event has occurred.
  • Message Queue: A queue structure that can store messages to be sent. Linux provides both System V and POSIX message queues.
  • Shared Memory: The fastest IPC mechanism, allowing multiple processes to share a memory region. Linux also provides both System V and POSIX shared memory.
  • Semaphore: Mainly used for synchronization and mutual exclusion, preventing multiple processes from accessing shared resources simultaneously. Linux provides both System V and POSIX semaphores.
  • Socket: Can be used for process communication between different machines, as well as for process communication on the same machine (Unix Domain Socket).

These IPC mechanisms each have their own advantages and disadvantages. The choice of which mechanism to use depends on the specific application requirements. For example, if cross-network communication is needed, sockets might be the best choice. If high-speed communication is needed, shared memory might be the best choice. If it is just simple parent-child process communication, a pipe might be sufficient.

For example, the commonly used command-line | is an anonymous pipe. It can take the output of one command as the input of another, allowing the two commands to work together, such as ls -l | grep "txt", cat file.txt | wc -l.

Pipes

In Linux systems, pipes are typically anonymous, meaning they do not have a visible path in the file system, but are implemented through special file descriptors created in the kernel.

To create anonymous pipes, use the system call int pipe(int fd[2]), which creates an anonymous pipe and returns two descriptors. One is the read end descriptor of the pipe fd[0], and the other is the write end descriptor fd[1]. Anonymous pipes are special files that exist only in memory, not in the file system.

One thing to note here is that when a process creates an anonymous pipe, both descriptors of that pipe are in the same process, so how does it enable cross-process communication?

In practice, anonymous pipes are used for communication between processes that have a parent-child relationship. When we call fork() to create a child process and the parent and child need to communicate, they can communicate through the shared file descriptors.

Typically, the parent process closes the read end of the pipe, and the child process closes the write end. This way, the parent only writes and the child only reads, avoiding simultaneous writing or simultaneous reading.

After communication is complete, the parent and child processes should close the file descriptors they no longer need. Closing the write end (or read end) of the pipe causes the corresponding read end (or write end) to receive an end-of-file marker, so the read operation returns 0, indicating that all data has been read.

//匿名管道示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1

int main() {
    char write_msg[BUFFER_SIZE] = "Hello, pipe!";
    char read_msg[BUFFER_SIZE];
    int fd[2];
    pid_t pid;

    // 创建管道
    if (pipe(fd) == -1) {
        fprintf(stderr, "Pipe failed");
        return 1;
    }

    // 创建子进程
    pid = fork();

    if (pid < 0) {
        fprintf(stderr, "Fork failed");
        return 1;
    }

    if (pid > 0) {  // 父进程
        close(fd[READ_END]);  // 关闭读取端

        // 写入数据到管道
        write(fd[WRITE_END], write_msg, strlen(write_msg) + 1);
        close(fd[WRITE_END]);  // 关闭写入端
        printf("Parent process wrote to the pipe: %s\n", write_msg);
    } else {  // 子进程
        close(fd[WRITE_END]);  // 关闭写入端

        // 从管道中读取数据
        read(fd[READ_END], read_msg, BUFFER_SIZE);
        printf("Child process read from the pipe: %s\n", read_msg);
        close(fd[READ_END]);  // 关闭读取端
    }

    return 0;
}

A named pipe (Named Pipe, or FIFO) is a special file with a name, stored in the file system and accessible through a file system path. It allows communication between unrelated processes and is created with the mkfifo command or the mkfifo() function.

One process can write data to a named pipe, and another process can read data from it. Its operations resemble ordinary file I/O and use functions such as open(), read(), write(), and close().

The main feature that distinguishes named pipes from anonymous pipes is persistence: even if the process that created it terminates, the named pipe remains in the file system until it is explicitly deleted.

A pipe is essentially a buffer inside the kernel.

When a process writes data to a pipe, the kernel buffers the data in the pipe’s internal buffer. If the pipe’s buffer is full, the write operation blocks until there is enough space to write the data. Similarly, when a process reads data from a pipe, the kernel reads data from the pipe’s internal buffer and provides it to the process. If the pipe’s buffer is empty, the read operation blocks until data is available to read.

Pipe read and write operations usually involve process synchronization and notification mechanisms. The kernel needs to ensure that when multiple processes access a pipe simultaneously, data reads and writes are safe and correct. To achieve process synchronization and notification, the Linux kernel may use semaphores, locks, or other synchronization mechanisms to protect pipe read and write operations, ensuring data consistency and correctness.

Therefore, pipes are inefficient as a communication method and are not suitable for frequent data exchange between processes.

Overall, as a basic inter-process communication mechanism, pipes are simple and convenient to use, but they also have some drawbacks, mainly reflected in:

  1. One-way communication: if two-way communication is needed, two pipes must be created, increasing complexity and overhead;
  2. Capacity limits: a pipe has a fixed capacity limit, generally determined by the operating system configuration. Once the buffer reaches its capacity limit, write operations block, which may cause IPC latency or deadlock;
  3. Blocking reads and writes: when a pipe is empty or full, read and write operations block, which can suspend processes and reduce system responsiveness;
  4. No data integrity guarantees: pipes only provide data stream transport, with no guarantee of integrity or reliability. Additional mechanisms such as checksums and acknowledgments usually need to be implemented at the application layer;
  5. Cannot be used over a network: pipes only work between processes on the same host and cannot be used for network communication.

Message Queues

For scenarios requiring more efficient, frequent data transfer, message queues can be used. In Linux, a message queue is a linked list of messages stored in the kernel.

The data transfer unit is user-defined data. The sender and receiver must agree on the message body data type before transmission. Each message body is a fixed-size storage block, unlike a pipe’s unformatted byte stream.

Its lifetime follows the system kernel. If the message queue is not explicitly released or the operating system is not shut down, the message queue persists.

Although message queues can conveniently transfer data between processes, they are not suitable for large data transfers, because in the kernel each message body has a maximum length limit, and the total length of all message bodies in a queue also has an upper limit. The kernel defines two macros, MSGMAX and MSGMNB, in bytes, which define the maximum length of a single message and the maximum length of a queue, respectively.

Additionally, in terms of runtime efficiency, the communication process incurs data copy overhead between user space and kernel space, because when a process writes data to a message queue in the kernel, the data is copied from user space to kernel space, and vice versa.

Therefore, for these two reasons, message queues are better suited for small-scale, low-frequency data transfer, such as task queues (where each message represents a pending task), log data, status updates, request-response patterns, and data streams (where data flows between multiple processes and each message contains part of the stream).

Primary use cases:

  1. Asynchronous processing: Message queues can be used to implement asynchronous processing, allowing a process to place a task into a queue and return immediately without waiting for the task to complete. This is especially useful for long-running tasks such as large-scale data computation or complex file operations.
  2. Load balancing: When there are many tasks to process, message queues can be used to distribute them. Each worker process can take one task from the queue, process it, and then take the next one. This keeps all worker processes busy and allows more workers to be added as needed.
  3. Decoupling: Message queues can be used to decouple different parts of a system. This means a change in one part does not directly affect other parts. For example, a service can publish messages to a queue without needing to know which consumers will receive and process them.
  4. Fault tolerance: If a process handling a message fails, the message can remain in the queue and be reprocessed by another process. This improves system reliability.
  5. Logging: Message queues can be used to collect system log information. Applications can send log messages to a queue, and a dedicated logging service can then read and process those messages from the queue.

Common system call interfaces for message queues: mq_open() / mq_send() / mq_receive() / mq_close() / mq_unlink()

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

#define QUEUE_NAME "/my_message_queue"
#define MAX_MSG_SIZE 256
#define MAX_MSG_COUNT 10

int main() {
    mqd_t mq;
    struct mq_attr attr;
    char buffer[MAX_MSG_SIZE + 1];
    int msg_flags = O_CREAT | O_RDWR;
    mode_t mode = S_IRUSR | S_IWUSR; // Permissions for the message queue

    // Set up the attributes of the message queue
    attr.mq_maxmsg = MAX_MSG_COUNT;
    attr.mq_msgsize = MAX_MSG_SIZE;
    attr.mq_flags = 0;

    // Create the message queue
    mq = mq_open(QUEUE_NAME, msg_flags, mode, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        exit(1);
    }

    printf("Message queue created.\n");

    // Send a message to the queue
    printf("Enter message to send: ");
    fgets(buffer, MAX_MSG_SIZE, stdin);

    if (mq_send(mq, buffer, strlen(buffer), 0) == -1) {
        perror("mq_send");
        exit(1);
    }

    printf("Message sent.\n");

    // Receive a message from the queue
    ssize_t bytes_read = mq_receive(mq, buffer, MAX_MSG_SIZE, NULL);
    if (bytes_read == -1) {
        perror("mq_receive");
        exit(1);
    }

    buffer[bytes_read] = '\0';
    printf("Received message: %s\n", buffer);

    // Close the message queue
    mq_close(mq);

    // Remove the message queue
    mq_unlink(QUEUE_NAME);

    return 0;
}

Shared memory

Modern operating systems manage memory with virtual memory, so each process has its own independent virtual memory space, and different processes’ virtual memory maps to different physical memory. Even if process A and process B have the same virtual address, the physical memory addresses they access are different, and their data operations do not affect each other.

Shared memory, on the other hand, is about mapping the same virtual address space to the same physical memory, so different processes can jointly create, read, update, and delete in the same memory region. This avoids copying and user-kernel mode switches, greatly improving IPC speed. Compared with message queues, the size of transferred data is limited only by the system’s physical memory, so its ability to handle large data is far greater than message queues. It is commonly used in database systems, image processing, and scientific computing.

When using shared memory as an IPC mechanism, you need to pay close attention to synchronization and security issues. Multiple processes accessing shared memory simultaneously can lead to data inconsistency, so synchronization mechanisms such as semaphores are needed to ensure data consistency. In addition, any process that can access the shared memory can modify the data, which can easily cause security problems such as accidental or malicious tampering. Therefore, when creating shared memory, you can enforce access control through permissions, and perform appropriate integrity checks when reading and writing data to enhance security.

Here is a simple example. First, the process that creates the shared memory and writes data to it:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <semaphore.h>

int main()
{
    const int SIZE = 4096;
    const char *name = "OS";
    const char *message_0 = "Hello";
    const char *message_1 = "World!";
    const char *sem_name = "sem";

    int shm_fd;
    void *ptr;
    sem_t *sem;

    /* 创建共享内存对象 */
    shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("In shm_open");
        exit(1);
    }

    /* 配置共享内存对象的大小 */
    if (ftruncate(shm_fd, SIZE) == -1) {
        perror("In ftruncate");
        exit(1);
    }

    /* 将共享内存对象映射到内存 */
    ptr = mmap(0, SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        perror("In mmap");
        exit(1);
    }

    /* 创建信号量 */
    sem = sem_open(sem_name, O_CREAT, 0666, 0);
    if (sem == SEM_FAILED) {
        perror("In sem_open");
        exit(1);
    }

    /* 将数据写入共享内存对象 */
    sprintf(ptr, "%s", message_0);
    ptr += strlen(message_0);
    sprintf(ptr, "%s", message_1);
    ptr += strlen(message_1);

    /* 通过信号量通知其他进程可以读取数据 */
    if (sem_post(sem) == -1) {
        perror("In sem_post");
        exit(1);
    }

    return 0;
}

Then the process that reads the data and removes the shared memory:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <semaphore.h>

int main()
{
    const int SIZE = 4096;
    const char *name = "OS";
    const char *sem_name = "sem";

    int shm_fd;
    void *ptr;
    sem_t *sem;

    /* 打开共享内存对象 */
    shm_fd = shm_open(name, O_RDONLY, 0666);
    if (shm_fd == -1) {
        perror("In shm_open");
        exit(1);
    }

    /* 将共享内存对象映射到内存 */
    ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        perror("In mmap");
        exit(1);
    }

    /* 打开信号量 */
    sem = sem_open(sem_name, 0);
    if (sem == SEM_FAILED) {
        perror("In sem_open");
        exit(1);
    }

    /* 等待信号量 */
    if (sem_wait(sem) == -1) {
        perror("In sem_wait");
        exit(1);
    }

    /* 从共享内存中读取数据 */
    printf("%s\n", (char *)ptr);

    /* 删除共享内存对象 */
    if (shm_unlink(name) == -1) {
        perror("In shm_unlink");
        exit(1);
    }

    /* 删除信号量 */
    if (sem_unlink(sem_name) == -1) {
        perror("In sem_unlink");
        exit(1);
    }

    return 0;
}

In this example, a semaphore is used to synchronize the operations of the two processes. After writing data, the writer process notifies the reader process through the semaphore that it can read the data. The reader process waits on the semaphore before reading. In addition, some error handling and safety checks are added, such as checking the return values of shm_open, ftruncate, mmap, sem_open, sem_post, sem_wait, shm_unlink and sem_unlink, printing an error message and exiting when an error occurs.

It is clear here that, compared with the previous two IPC mechanisms, achieving higher efficiency requires paying attention to more details. There are several key points to focus on:

  1. Synchronized access: when multiple processes access shared memory, you must use some synchronization mechanism, such as semaphores or mutexes, to ensure data consistency and integrity.
  2. Clean up shared memory: When no process needs the shared memory anymore, it should be removed to free system resources. On Linux, you can use the shmctl function (for System V shared memory) or the shm_unlink function (for POSIX shared memory) to remove shared memory.
  3. Error handling: When using shared memory, check all possible error conditions and handle errors appropriately.
  4. Avoid overly large shared memory regions: Although shared memory is an efficient IPC mechanism, using overly large shared memory regions can consume significant system resources and may cause performance issues.
  5. Use appropriate data structures: In shared memory, use data structures suitable for concurrent access. For example, if multiple processes need to read and write a data structure simultaneously, use a structure that supports concurrent access, such as a linked list or hash table.
  6. Avoid using pointers: In shared memory, do not use pointers to non-shared memory regions, because those pointers may be invalid in other processes.
  7. Security: Shared memory can be accessed by any process with appropriate permissions, so data security should be considered. If needed, use encryption and decryption mechanisms to protect the data.

Semaphore

In the example code above, besides shared memory, another IPC mechanism is introduced: the semaphore, which solves the synchronization problem of shared memory access.

A semaphore is essentially an integer counter, mainly used to implement mutual exclusion and synchronization between processes, rather than for buffering data communicated between processes.

Its definition in the Linux kernel is:

struct semaphore {
    raw_spinlock_t      lock;
    unsigned int        count;
    struct list_head    wait_list;
};

Where:

  • lock is a raw spinlock used to protect the semaphore data structure.
  • count is the current value of the semaphore. When a process calls down or sem_wait to try to acquire the semaphore, if count is greater than 0, then count is decremented by 1 and the process continues executing. If count equals 0, the process is blocked until count becomes non-zero.
  • wait_list is a linked list containing all processes waiting on this semaphore.

There are two atomic operations for controlling semaphores: the P operation and the V operation.

  • The P operation is also called the “wait” or “down” operation. When a process needs to access a shared resource, it performs the P operation, in which the semaphore value is decremented by 1.
  • The V operation is also called the “signal” or “up” operation. When a process finishes accessing a shared resource, it performs the V operation, in which the semaphore value is incremented by 1.

The terms P operation and V operation originate from Dutch. The P operation comes from “Proberen”, meaning “to try”, and the V operation comes from “Verhogen”, meaning “to increase”.

An atomic operation is one that cannot be interrupted in a multithreaded environment. In other words, it either executes completely or not at all, never partially. During execution, it cannot be interrupted by other threads.

As mentioned earlier, a semaphore is essentially a counter. When initialized to 1, the first process to perform a P operation acquires the resource, and all subsequent P operations must block and wait, ensuring that only one process accesses the shared memory at any given time. In this case, the semaphore acts as a mutex semaphore.

What if it is initialized to 0?

In multi-process scenarios, each process typically runs independently with an unpredictable execution order. When we want multiple processes to cooperate on a task, we can initialize the semaphore to 0. For example, if process A is a data producer and process B is a data consumer, B clearly depends on A. If B runs before A and reaches its P operation, it blocks because the semaphore value is 0. It stays blocked until A performs a V operation, which effectively wakes up the process blocked on the P operation. In this case, the semaphore acts as a synchronization semaphore, ensuring that process A’s V operation executes before process B’s P operation.

This is the classic producer-consumer problem and one of the most common use cases for semaphores.

Commonly used semaphore operations include sem_init()/sem_wait()/sem_post()/sem_destroy() and others, which can be found in the semaphore.h header file. Here is a brief reference:

  1. int sem_init(sem_t *sem, int pshared, unsigned int value);:
    • Initialize a new semaphore.
    • sem: Pointer to the semaphore to initialize.
    • pshared: Specifies whether the semaphore is shared. A value of 0 means the semaphore is shared within the process; a non-zero value means it is shared between processes (typically used for inter-thread communication).
    • value: Specifies the initial value of the semaphore.
  2. int sem_wait(sem_t *sem);:
    • Wait on the semaphore. If its value is greater than 0, decrement it by one. If its value is 0, block until the value becomes greater than 0.
    • sem: Pointer to the semaphore to operate on.
  3. int sem_post(sem_t *sem);:
    • Releases the semaphore, incrementing its value by one.
    • sem: Pointer to the semaphore to operate on.
  4. int sem_destroy(sem_t *sem);:
    • Destroys the semaphore and releases resources associated with it.
    • sem: Pointer to the semaphore to destroy.

When using semaphores, there are two points to keep in mind: first, handle boundary conditions carefully to avoid deadlocks; second, ensure semaphores are released promptly after use to avoid resource leaks. Take the following code as an example.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

#define BUFFER_SIZE 5

sem_t mutex, empty, full;
int buffer[BUFFER_SIZE];
int in = 0, out = 0;

void *producer(void *arg) {
    int item = 0;
    while (1) {
        item = rand() % 100;

        sem_wait(&empty);
        sem_wait(&mutex);

        buffer[in] = item;
        printf("Produced item: %d\n", item);
        in = (in + 1) % BUFFER_SIZE;

        sem_post(&mutex);
        sem_post(&full);

        // Sleep for random time
        sleep(rand() % 3);
    }
}

void *consumer(void *arg) {
    int item = 0;
    while (1) {
        sem_wait(&full);
        sem_wait(&mutex);

        item = buffer[out];
        printf("Consumed item: %d\n", item);
        out = (out + 1) % BUFFER_SIZE;

        sem_post(&mutex);
        sem_post(&empty);

        // Sleep for random time
        sleep(rand() % 3);
    }
}

int main() {
    // Initialize semaphores
    sem_init(&mutex, 0, 1);
    sem_init(&empty, 0, BUFFER_SIZE);
    sem_init(&full, 0, 0);

    // Create producer and consumer threads
    pthread_t producer_thread, consumer_thread;
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);

    // Join threads
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // Destroy semaphores
    sem_destroy(&mutex);
    sem_destroy(&empty);
    sem_destroy(&full);

    return 0;
}

In this example, the boundary conditions mainly involve buffer usage and the initial values of the semaphores.

  1. Buffer boundaries:
    • When the producer writes data into the buffer, it must ensure the buffer does not overflow. That is, when the in pointer exceeds the buffer boundary, set it to 0 to implement a circular buffer.
    • When the consumer reads data from the buffer, it must ensure it does not read invalid data. That is, when the out pointer exceeds the buffer boundary, set it to 0 to implement a circular buffer.
  2. Semaphore initial values:
    • empty The initial value of the semaphore should equal the buffer size, representing the number of free slots available in the buffer.
    • full The initial value of the semaphore should be 0, representing the amount of data already stored in the buffer.

By handling these boundary conditions correctly, you can ensure that producer and consumer threads do not overflow or go out of bounds when accessing the shared resource (the buffer), preserving the correctness and stability of the program.

Signals

The IPC mechanisms above are mainly used when a program is running normally. When a program behaves abnormally, another IPC mechanism, signals, is needed for cross-process communication.

Signals allow one process to send a notification to another process, telling it that an event has occurred or requesting that it perform an action. Signals are a lightweight communication method, commonly used for asynchronous event handling, inter-process synchronization, and exception handling.

Signals have four basic characteristics:

  1. Numbering: Each signal has a unique number, usually represented as an integer. For example, SIGINT represents the interrupt signal.
  2. Sending: A process can send a signal to another process by calling the kill() function, or by typing specific terminal control characters (for example, Ctrl+C sends the SIGINT signal).
  3. Handling: The process receiving the signal can choose to ignore it, perform the default action, or register a signal handler.
  4. Asynchronous: Signals are asynchronous events, meaning there is no direct communication channel between the sending process and the receiving process.

Common system signals (some signals may differ across systems):

  • SIGINT: Interrupt signal, usually sent by the user pressing Ctrl+C, used to interrupt a process.
  • SIGTERM: Termination signal, used to request that a process terminate normally.
  • SIGKILL: Force termination signal, used to terminate a process immediately.
  • SIGUSR1 and SIGUSR2: User-defined signals that applications can customize.
  • SIGSEGV: Segmentation fault signal, indicating that a process accessed an invalid memory address.
  • SIGCHLD: Child process status change signal, used to notify the parent process of a status change in a child process.

For each signal, the operating system defines a default handling action, such as terminating the process or ignoring the signal. However, a process can register a custom signal handler for a specific signal, and when that signal is received, the corresponding handling logic is executed. Additionally, signal masking can be used to temporarily block certain signals to avoid being interrupted at critical moments. Signal operations should be lightweight and fast; their handlers should be kept as simple and safe as possible, avoiding calls to non-reentrant functions or performing complex or blocking operations.

Basic usage is as follows:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
    printf("Received signal: %d\n", signum);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, signal_handler); // Ctrl+C中断信号
    signal(SIGTERM, signal_handler); // 终止信号

    // 进入无限循环等待信号
    while(1) {
        sleep(1);
    }

    return 0;
}

Socket

The previous five IPC mechanisms all perform inter-process communication on the same host, while Socket enables inter-process communication across networks and hosts. Of course, it can also be used for inter-process communication on the same host. In the IPC mechanisms introduced earlier, processes are uniquely identified by PID during inter-process communication. In a cross-host scenario, how does Socket uniquely identify a process?

On the surface, a Socket is a special file in the Linux filesystem and a set of APIs, but in reality it is also an application-layer abstraction of the TCP/IP protocol suite. With TCP/IP, a host on the network can be uniquely identified by its network-layer IP address, and a process on the host can be uniquely identified by the transport-layer protocol and port. IP + protocol + port is how Socket uniquely identifies a process.

The Socket API call flow is affected by the communication type chosen when creating the Socket. This is the system call for creating a socket:

int socket(int domain, int type, int protocal)

  • domain: Specifies the address family. Common ones include AF_INET (IPv4 address family) and AF_INET6 (IPv6 address family), among others.
  • type: Specifies the Socket type. Common ones include SOCK_STREAM (stream socket, used for TCP) and SOCK_DGRAM (datagram socket, used for UDP), among others.
  • protocol: specifies the protocol; usually 0 means use the default protocol.

When the created socket type is TCP, the server needs to listen on the socket to wait for connection requests. When a client sends a connection request, both sides perform a three-way handshake. After the connection succeeds, a new socket is created for communicating with the client, and then formal data transfer begins. The following is a simple TCP socket example, including a simple server and client, demonstrating how to establish a TCP connection and transfer data:

//tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char *welcome_message = "Welcome to the server!";

    // 创建TCP Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 等待连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 发送欢迎消息给客户端
    send(new_socket, welcome_message, strlen(welcome_message), 0);
    printf("Welcome message sent to client\n");

    // 接收客户端消息并回复
    int valread;
    if ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        printf("Client: %s\n", buffer);
        send(new_socket, buffer, strlen(buffer), 0);
    }

    printf("Closing connection...\n");
    close(new_socket);
    close(server_fd);

    return 0;
}
//tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0, valread;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char *message = "Hello from client";

    // 创建TCP Socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        exit(EXIT_FAILURE);
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将IP地址转换为网络字节序
    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
        perror("Invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }

    // 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection failed");
        exit(EXIT_FAILURE);
    }

    // 发送消息给服务器
    send(sock, message, strlen(message), 0);
    printf("Message sent to server\n");

    // 接收服务器的回复
    valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server: %s\n",buffer);

    close(sock);
    return 0;
}

During TCP socket communication, reliable data transfer is guaranteed by the TCP protocol, which ensures ordered delivery and reliability of data. Therefore, when using TCP sockets for communication, there is no need to worry much about data loss or ordering issues; you only need to focus on sending and receiving data correctly.

When the created socket type is UDP, unlike TCP, UDP does not establish a connection in advance (that is, no handshake), nor does it maintain connection state information. This means each packet is independent, and both sending and receiving are stateless. In turn, this mechanism gives it the advantage of low latency and high efficiency, making it suitable for scenarios with high real-time data transfer requirements. The following is a simple UDP socket example, including a server and a client, demonstrating how to use UDP sockets for communication:

//udp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buffer[BUFFER_SIZE];

    // 创建UDP Socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 设置服务器地址信息
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 将Socket绑定到地址和端口上
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    int len, n;
    len = sizeof(cliaddr);

    while (1) {
        // 接收数据
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
        buffer[n] = '\0';
        printf("Client : %s\n", buffer);

        // 发送数据
        sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
        printf("Message sent to client.\n");
    }

    return 0;
}
//udp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[BUFFER_SIZE];
    char *message = "Hello from client";

    // 创建UDP Socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    // 设置服务器地址信息
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    int n, len;
    len = sizeof(servaddr);

    // 发送数据
    sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM, (const struct sockaddr *)&servaddr, len);
    printf("Message sent to server.\n");

    // 接收回复
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("Server : %s\n", buffer);

    close(sockfd);
    return 0;
}

POSIX & System V

POSIX (Portable Operating System Interface) and System V are two different Unix standards. They differ in many ways, including the inter-process communication (IPC) mechanisms they provide.

The following are some of their main differences in IPC:

  1. Message queues: System V provides msgget, msgsnd, msgrcv and msgctl system calls to operate on message queues. POSIX provides mq_open, mq_send, mq_receive, mq_close and mq_unlink functions to operate on message queues. POSIX message queues support priorities, while System V message queues support message types.
  2. Semaphores: System V provides semget, semop and semctl system calls to operate on semaphores. POSIX provides sem_open, sem_wait, sem_post, sem_close and sem_unlink functions to operate on semaphores. POSIX semaphores can be used between processes or threads, while System V semaphores are mainly used between processes.
  3. Shared memory: System V provides the shmget, shmat, shmdt and shmctl system calls to operate on shared memory. POSIX provides the shm_open, mmap, munmap, shm_unlink function to operate on shared memory.
  4. Naming and lifecycle: System V IPC objects are identified by key and id, and must be explicitly deleted; otherwise they persist. POSIX IPC objects are identified by name and can be configured for automatic deletion when the last reference is closed.
  5. Interface: POSIX interfaces are generally simpler and easier to use, while System V interfaces are more complex and offer more options and features.

Which IPC mechanism to choose depends mainly on the specific application requirements and developer preference.

In general, we may lean toward POSIX IPC mechanisms, mainly because:

  1. Interface consistency: POSIX IPC interfaces remain consistent across different Unix-like systems, including Linux, which helps improve code portability.
  2. More modern features: Compared to System V, POSIX IPC mechanisms offer more modern features, such as better thread support, more flexible naming, and better resource management.

However, that does not mean System V IPC mechanisms have no place. In certain scenarios, System V IPC mechanisms may be the better choice, for example:

  1. Richer functionality: System V IPC mechanisms provide some features that POSIX IPC mechanisms lack, such as message types in message queues and atomicity of semaphore operations.
  2. Broader compatibility: Because System V IPC mechanisms have a longer history, they may be better supported on some older systems.

Overall, Linux does not default to either POSIX or System V; instead, it provides both IPC mechanisms for developers to choose based on their specific needs and preferences.