进程通信
进程通信( InterProcess Communication,IPC)就是指进程之间的信息交换。实际上,进程的同步与互斥本质上也是一种进程通信(这也就是待会我们会在进程通信机制中看见信号量和 PV 操作的原因了),只不过它传输的仅仅是信号量,通过修改信号量,使得进程之间建立联系,相互协调和协同工作,但是它缺乏传递数据的能力。
虽然存在某些情况,进程之间交换的信息量很少,比如仅仅交换某个状态信息,这样进程的同步与互斥机制完全可以胜任这项工作。但是大多数情况下,进程之间需要交换大批数据,比如传送一批信息或整个文件,这就需要通过一种新的通信机制来完成,也就是所谓的进程通信。
再来从操作系统层面直观的看一些进程通信:我们知道,为了保证安全,每个进程的用户地址空间都是独立的,一般而言一个进程不能直接访问另一个进程的地址空间,不过内核空间是每个进程都共享的,所以进程之间想要进行信息交换就必须通过内核。
Linux 内核提供的常见的进程通信机制:
- 管道(也称作共享文件)
- 消息队列(也称作消息传递)
- 共享内存(也称作共享存储)
- 信号量和 PV 操作
- 信号
- 套接字(Socket)
一、 管道
1. 匿名管道
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。如果想实现相互通信(全双工通信),我们需要创建两个管道才行。另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。并且,匿名管道只能在具有亲缘关系(父子进程)的进程间使用。也就是说,匿名管道只能用于父子进程之间的通信。
在 Linux 的实际编码中,是通过 pipe 函数来创建匿名管道的,若创建成功则返回 0,创建失败就返回 -1。fork()调用后,操作系统会复制父进程的地址空间到子进程,包括代码段、数据段、堆栈等。这意味着子进程拥有父进程的内存空间的一个副本。fork()返回两次,一次在父进程中,返回子进程的PID;一次在子进程中,返回0。如果fork()调用失败,它会在父进程中返回-1,并且设置errno来指示错误:
如果pid1 == 0,则当前代码运行在子进程中。
如果pid1 > 0,则当前代码运行在父进程中。:
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
pid_t pid1;
int fields[2];
char buffer[80];
char s[100];
char ss[100];
if (pipe(fields) != 0)
{
fprintf(stderr, "Createpipe error:%s\n\a", strerror(errno));
exit(1);
}
if ((pid1 = fork()) < 0)
printf("fork child error!\n");
/* 子进程写入数据 */
if (pid1 == 0)
{
printf("fork child,pid is %d, child is sending a message !\n", pid1);
char s[] = "hello!\n";
write(fields[1], s, sizeof(s));
exit(0);
}
/* 父进程读取数据 */
else
{
printf("parent read start, pid is %d!\n", pid1);
read(fields[0], buffer, 80);
printf("parent receive the message:%s", buffer);
}
exit(0);
}
2.命名管道
命名管道fifo解决了pipe只能有关系的进程才能通信的问题,实现一个有名管道实际上就是实现一个FIFO文件,有名管道一旦建立,之后它的读,以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在内核缓冲页面上,和普通管道相同。
使用 Linux 命令 mkfifo 来创建有名管道:
mkfifo myPipe
myPipe 就是这个管道的名称,接下来,我们往 myPipe 这个有名管道中写入数据:
echo "hello" > myPipe
执行这行命令后,你会发现它就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。于是,我们执行另外一个命令来读取这个有名管道里的数据:
cat < myPipe
读进程,定义一个结束消息,保证在写进程先退出时读进程不会发生疯读现象:
// 读进程
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define FIFO_PATH "myfifofile"
#define END_MESSAGE "END" // 定义结束消息
int main()
{
int fd;
char cont_r[256]; // 增加1字节来存储结束字符
// 创建命名管道
if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)
{
perror("create fifo failed");
return -1;
}
else
{
printf("create fifo success\n");
// 打开文件进行读操作
fd = open(FIFO_PATH, O_RDONLY, 0666);
if (fd > 0)
{
while (1)
{
// 读取数据
ssize_t bytes_read = read(fd, cont_r, sizeof(cont_r) - 1);
if (bytes_read <= 0)
{
// 如果没有数据可读,退出循环
break;
}
// 确保字符串以空字符结尾
cont_r[bytes_read] = '\0';
// 打印读取到的数据
printf("read: %s\n", cont_r);
// 检查是否接收到结束消息
if (strcmp(cont_r, END_MESSAGE) == 0)
{
printf("Received end message, exiting...\n");
break;
}
}
close(fd);
}
else
perror("open failed");
}
return 0;
}
写进程:
// 写进程
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define FIFO_PATH "myfifofile"
int main()
{
int fd;
char cont_w[] = "hello sundy";
const char *end_message = "END"; // 结束消息
if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)
{
perror("create fifo failed");
return -1;
}
else
{
printf("create fifo success\n");
fd = open(FIFO_PATH, O_CREAT | O_WRONLY, 0666);
if (fd > 0)
{
int write_count = 0;
const int max_writes = 5; // 写入次数限制
while (write_count < max_writes)
{
write(fd, cont_w, strlen(cont_w));
printf("write success\n");
sleep(2);
write_count++;
}
// 发送结束消息
write(fd, end_message, strlen(end_message));
close(fd); // 关闭文件描述符
}
else
perror("open failed");
}
return 0;
}
二、消息队列
可以看出,管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此,消息传递机制(Linux 中称消息队列)应用而生。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程在需要的时候自行去消息队列中读取数据就可以了。同样的,B 进程要给 A 进程发送消息也是如此。
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。对比一下管道机制:
- 消息队列允许一个或多个进程向它写入或读取消息。
- 消息队列可以实现消息的随机查询,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。比有名管道的先进先出原则更有优势。
- 对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。而对于管道来说,除非读进程已存在,否则先有写进程进行写入操作是没有意义的。
- 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道随进程的创建而建立,随进程的结束而销毁。
需要注意的是,消息队列对于交换较少数量的数据很有用,因为无需避免冲突。但是,由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,另一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入。
//创建获取内核对象
int msgget(key_t key, int flags);
// 将消息发送到消息队列
int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
// 从消息队列获取消息
ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 查看、删除、设置 ipc 内核对象(用法和shmctl相同)
int msgctl(int msgid, int cmd, struct msqid_ms *buf);
读进程,销毁队列时不能确定队列是否仍然存在,所以销毁需要保证仅在读进程发生:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
// 消息队列数据结构
struct mesg_buffer
{
long mesg_type;
char mesg_text[100];
} message;
int main()
{
key_t key;
int msgid;
int bytes_read;
// 使用ftok生成唯一的key
key = ftok("progfile", 65);
// msgget创建消息队列,并返回标识符
if ((msgid = msgget(key, 0666 | IPC_CREAT)) < 0)
{
perror("msgget failed");
exit(1);
}
message.mesg_type = 1;
printf("Write Data : ");
// 使用fgets代替gets,限制输入长度
if (fgets(message.mesg_text, sizeof(message.mesg_text), stdin) == NULL)
{
perror("fgets failed");
exit(1);
}
// 去掉fgets读取的换行符
message.mesg_text[strcspn(message.mesg_text, "\n")] = 0;
// msgsnd发送消息
if (msgsnd(msgid, &message, strlen(message.mesg_text), 0) < 0)
{
perror("msgsnd failed");
exit(1);
}
// 显示发送的消息
printf("Data send is : %s \n", message.mesg_text);
return 0;
}
写进程:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
// 消息队列数据结构
struct mesg_buffer
{
long mesg_type;
char mesg_text[100];
} message;
int main()
{
key_t key;
int msgid;
int bytes_read;
// 使用ftok生成唯一的key
key = ftok("progfile", 65);
// msgget创建消息队列,并返回标识符
if ((msgid = msgget(key, 0666 | IPC_CREAT)) < 0)
{
perror("msgget failed");
exit(1);
}
message.mesg_type = 1;
printf("Write Data : ");
// 使用fgets代替gets,限制输入长度
if (fgets(message.mesg_text, sizeof(message.mesg_text), stdin) == NULL)
{
perror("fgets failed");
exit(1);
}
// 去掉fgets读取的换行符
message.mesg_text[strcspn(message.mesg_text, "\n")] = 0;
// msgsnd发送消息
if (msgsnd(msgid, &message, strlen(message.mesg_text), 0) < 0)
{
perror("msgsnd failed");
exit(1);
}
// 显示发送的消息
printf("Data send is : %s \n", message.mesg_text);
return 0;
}
三、共享内存
为了避免像消息队列那样频繁的拷贝消息、进行系统调用,共享内存机制出现了。顾名思义,共享内存就是允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
集合内存管理的内容,我们来深入理解下共享内存的原理。首先,每个进程都有属于自己的进程控制块(PCB)和逻辑地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。
不同于消息队列频繁的系统调用,对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。
相关函数:
// 申请共享内存
int shmget(key_t __key, size_t __size, int __shmflg)
// 建立用户进程空间到共享内存的映射
void *shmat(int __shmid, const void *__shmaddr, int __shmflg)
// 解除映射关系
int shmdt(const void *__shmaddr);
// 回收共享内存空间
int shmctl(int __shmid, int __cmd, shmid_ds *__buf);
读进程:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
key_t key = ftok("shmfile", 65);
// shmget returns an identifier in shmid
int shmid = shmget(key, 1024, 0666);
if (shmid == -1)
{
perror("shmget failed");
return 1; // 退出程序
}
// shmat to attach to shared memory
char *str = (char *)shmat(shmid, (void *)0, 0);
if (str == (char *)(-1))
{
perror("shmat failed");
return 1; // 退出程序
}
// 检查共享内存是否为空
if (strlen(str) == 0)
{
printf("No data in shared memory.\n");
}
else
{
printf("Data read from memory: %s\n", str);
}
// detach from shared memory
if (shmdt(str) == -1)
{
perror("shmdt failed");
return 1; // 退出程序
}
// destroy the shared memory
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
if (errno != EIDRM)
{ // 检查是否因为已被删除而失败
perror("shmctl failed");
return 1; // 退出程序
}
// 如果共享内存已被删除,打印消息
printf("Shared memory already destroyed.\n");
}
return 0;
}
写进程:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
int main()
{
key_t key = ftok("shmfile", 65);
// shmget returns an identifier in shmid
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1)
{
perror("shmget failed");
return 1; // 退出程序
}
// shmat to attach to shared memory
char *str = (char *)shmat(shmid, (void *)0, 0);
if (str == (char *)(-1))
{
perror("shmat failed");
return 1; // 退出程序
}
// 使用fgets代替gets来避免缓冲区溢出
printf("Enter data to write in memory: ");
if (!fgets(str, 1024, stdin))
{
perror("fgets failed");
shmdt(str); // 分离共享内存
return 1; // 退出程序
}
// 去掉fgets读取的换行符
size_t len = strlen(str);
if (len > 0 && str[len - 1] == '\n')
{
str[len - 1] = '\0';
}
printf("Data written in memory: %s\n", str);
// detach from shared memory without destroying it
if (shmdt(str) == -1)
{
perror("shmdt failed");
return 1; // 退出程序
}
// 注意:这里不删除共享内存,由读进程负责删除
return 0;
}
四、信号量和 PV 操作
实际上,对具有多 CPU 系统的最新研究表明,在这类系统上,消息传递的性能其实是要优于共享内存的,因为消息队列无需避免冲突,而共享内存机制可能会发生冲突。也就是说如果多个进程同时修改同一个共享内存,先来的那个进程写的内容就会被后来的覆盖。并且,在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进(异步性)。但有时候我们又希望多个进程能密切合作,按照某个特定的顺序依次执行,以实现一个共同的任务。
举个例子,如果有 A、B 两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据可读,因此读进程 A 被阻塞。
因此,为了解决上述这两个问题,保证共享内存在任何时刻只有一个进程在访问(互斥),并且使得进程们能够按照某个特定顺序访问共享内存(同步),我们就可以使用进程的同步与互斥机制,常见的比如信号量与 PV 操作。
信号量就是具有原子性的计数器,就相当于一把锁,在每个进程要访问临界资源时,必须要向信号量拿个锁”,它才能进去临界资源这个“房间”,并锁上门,不让其他进程进来,此时信号量执行P()操作,锁的数目减少了一个,所以计数器减1,;当它访问完成时,它出来,将锁还给信号量,执行V()操作,计数器加1;
程序源码示例:
让两个进程分别向显示器(linux下一切皆文件,临界资源)打印AA和BB,当没有信号量进行保护时,会出现数据混乱,例如:“AABBABAAAB…”,为了解决这一问题,我们创建信号量进行保护。打印“AA”或“BB”
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
// struct seminfo *buff;
};
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
int main(int argc, char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
/* 如果程序第一个被调用,也就是调用时含有一个参数,使得argc>1,此时就调用set_semvalue初始化信号量,并将op_char设置为x*/
if (argc > 1)
{
if (!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
/*进入和离开临界区10次,每次循环开始的时候首先调用semaphore_p函数,它在程序将进入临界区域时设置信号量以等待进入*/
for (i = 0; i < 10; i++)
{
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);
fflush(stdout);
/*进入临界区域后,调用semaphore_v将信号量设置为可用,然后等待一段随机的时间,再进入下一次循环*/
if (!semaphore_v())
exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (argc > 1)
{
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
/*该函数用来将semctl调用的command参数设置为SETVAL来初始化信号量*/
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return (1);
}
/*通过调用semctl调用的command设置为IPC_RMID来删除信号量ID*/
static void del_semvalue(void)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore");
}
/*对信号量执行减1操作*/
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return (0);
}
return (1);
}
/*对信号量执行加1操作*/
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return (0);
}
return (1);
}
变为父子进程间的信号量机制:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
// struct seminfo *buff;
};
int main(int argc, char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
int id = fork();
if (id < 0)
{
perror("fork failed\n");
return -1;
}
else if (id > 0)
{
if (!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
for (i = 0; i < 10; i++)
{
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);
fflush(stdout);
if (!semaphore_v())
exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (id > 0)
{
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return (1);
}
static void del_semvalue(void)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore");
}
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return (0);
}
return (1);
}
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return (0);
}
return (1);
}
五、信号
注意!信号和信号量是完全不同的两个概念!
信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。
信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说我们可以通过键盘输入某些组合键给进程发送信号,比如常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是通过 kill 系列的命令给进程发送信号,比如 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其立即结束。
程序源码示例
以模拟闹钟的形式,通过一个进程向另一个进程发送SIGALRM信号来表现进程间的通信。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int alarm_fired = 0;
/*该函数用来模拟闹钟*/
void ding(int sig)
{
alarm_fired = 1;
}
/*main函数中告诉子进程在等待5秒后发送SIGALRM信号给它的父进程*/
int main()
{
pid_t pid;
printf("alarm start\n");
pid = fork(); /*创建子进程*/
switch (pid)
{
case -1:
perror("fork failed");
exit(1);
case 0:
sleep(5); /*子进程休眠5秒*/
kill(getppid(), SIGALRM); /*子进程在5秒后将SIGALRM信号传递给父进程*/
exit(0);
}
/*父进程通过一个signal调用捕获SIGALRM信号的工作,等待该信号的到来*/
printf("waitting for alarm to go on\n");
(void)signal(SIGALRM, ding);
pause();
if (alarm_fired)
printf("ding!\n");
printf("done\n");
exit(0);
}
六、Socket
至此,上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要跨网络与不同主机上的进程进行通信,那该怎么做呢?这就是 Socket 通信做的事情了(当然,Socket 也能完成同主机上的进程通信)。
socket即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
socket_server:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int server_sockfd, client_sockfd, n;
pid_t pid;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd == -1)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(9736);
if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_sockfd, 5) == -1)
{
perror("listen failed");
exit(EXIT_FAILURE);
}
signal(SIGCHLD, SIG_IGN);
while (1)
{
printf("Server waiting\n");
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
if (client_sockfd == -1)
{
perror("accept failed");
continue;
}
pid = fork();
if (pid == -1)
{
perror("fork failed");
close(client_sockfd);
continue;
}
else if (pid == 0)
{
// Child process
close(server_sockfd); // Close the listener socket
n = read(client_sockfd, &buffer, 1);
if (n == -1)
{
perror("read failed");
}
else
{
// Process the data from client
buffer++;
sleep(5); // Simulate some processing
n = write(client_sockfd, &buffer, 1);
if (n == -1)
{
perror("write failed");
}
}
close(client_sockfd); // Close the client socket
exit(0);
}
else
{
// Parent process
close(client_sockfd); // Close the client socket in parent
}
}
close(server_sockfd); // Close the listener socket (should never reach here)
return 0;
}
写进程:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int sockfd;
struct sockaddr_in server_address;
char ch = 'A';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket creation failed");
exit(EXIT_FAILURE);
}
server_address.sin_family = AF_INET;
server_address.sin_port = htons(9736);
if (inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr) <= 0)
{
perror("invalid address");
close(sockfd);
exit(EXIT_FAILURE);
}
if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
write(sockfd, &ch, 1);
if (read(sockfd, &ch, 1) == -1)
{
perror("read failed");
}
printf("Char from server: %c\n", ch);
close(sockfd);
return 0;
}