Linux系统编程、网络编程
- 前言
- 一、进程的退出
- 二、消息队列
- 三、信号
- 四、线程
- 五.网络编程
- 1.字节序
- 2.Socket服务器和客户端的开发步骤
- 3.Socket连接使用的API详解
- 【服务器部分】— — — — — — — — — — — — — — — —
- 3.1 int socket() —— 指定讲“汉语”(连接协议TCP/UDP)
- 3.2 int bind() —— 地址准备好(我ip地址(楼号)是。我端口号(房间号)是。。)
- 3.3 listen() —— 监听(等待大家的来访,等别人敲门)
- 3.4 accept() —— 接受连接(同意别人进来房间)
- 3.5 数据收发
- 3.6 数据收发常用的第二套API(就比上面第一套的多一个flags参数,flags主要用来控制是否有阻塞等等。其它前面三个参数是一模一样)
- 【客户端部分】— — — — — — — — — — — — — — — —
- 3.7 客户端创建套接字socket()
- 3.8 客户端的connect函数
- 4.代码——Socket服务器和客户端的开发代码
前言
linux系统编程一些疑惑点,
linux网络编程实现
一、进程的退出
正常退出
①Main函数调用return
②进程调用exit(),标准c库
③进程调用_exit()或者_Exit(),属于系统调用
补充:
④进程最后一个线程返回
⑤最后一个线程调用pthread_exit
异常退出
1.主动调用API abort
2.当进程收到某些信号时,如ctrl+C
3.最后一个线程对取消(cancellation),请求做出响应
父进程等待子进程退出
为什么要等待子进程退出
父进程等待子进程退出,并收集子进程退出状态
子进程退出状态不被收集,变成僵尸进程
1.不关心子进程的退出状态:wait()
2.关心子进程的的退出状态:wait(&status) ,
子进程退出exit(2)括号里面的数字自己设,是一种标记。退出后如果父进程里有调用wait(&status)函数,wait会收集子进程exit(2)返回的标识2,存在status中。需要用WEXITSTATUS(status)来翻译出来。
二、消息队列
消息队列操作:
1 #include <sys/msg.h>
2 // 创建或打开消息队列:成功返回队列ID,失败返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失败返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 读取消息:成功返回消息数据的长度,失败返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息队列:成功返回0,失败返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
形象举例:
假设你有一个忙碌的家庭,父母和孩子们需要在家里留言通知彼此。父母有一个留言板,孩子们可以写上自己的消息并贴在留言板上。这里的留言板就相当于消息队列,孩子是发送者,父母是接收者。
msgget(key_t key, int flag):创建一个新的留言板,父母和孩子们需要确定留言板的ID号码以及权限,才能够使用这个留言板。key就是留言板的ID号码,flag就是设置留言板的权限。
msgsnd(int msqid, const void *ptr, size_t size, int flag):孩子们写好了消息,需要传递给留言板,msqid就是留言板的ID(哪一个留言板),ptr就是孩子们的消息内容,size就是消息的长度,flag就是发送消息的方式。
msgrcv(int msqid, void *ptr, size_t size, long type,int flag):父母需要去留言板上查看孩子们写的消息,msqid就是留言板的ID(哪一个留言板),ptr就是存放消息内容的地方,size就是接收消息的长度,type就是选择接收哪种类型的消息,flag就是接收消息的方式。
msgctl(int msqid, int cmd, struct msqid_ds *buf):父母需要对留言板进行一些操作,比如清空留言板或者删除留言板,msqid就是留言板的ID(哪一个留言板),cmd就是操作的指令,buf就是存放操作结果的地方。
疑惑点
疑惑点一
为什么下面这段代码里的msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);的第第三个参数不是sizeof(sendBuf),而只传了sendBuf.mtext的大小,那sendBuf结构体中的long mtype为什么不用传,为什么不是传整个结构体的长度?
//GPT回答:首先sizeof(sendBuf)包含了mtype和mtext的大小,但msgsnd()函数只需要消息内容(mtext)的大小,所以用strlen(sendBuf.mtext)。(消息类型)mtype不需要传入msgsnd()函数的长度参数中,因为mtype(数据类型)的作用只是:消息队列是根据mtype来进行消息分类和筛选的,msgsnd()函数在发送消息时会根据mtype进行筛选匹配。
//我简而言之:因为msgsnd函数中封装好了它做了处理,我们传&sendBuf地址给它,msgsnd函数内部会根据这个地址自动通过我们设置的类型 sendBuf.mtype 来进行消息分类和匹配,然后 msgsnd函数 内部再以 sendBuf.mtext 作为我们需要传送的数据为长度,并且我们也只需要发送这个数据。
msgSend.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
// int msgflg);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[256]; /* message data */
};
int main()
{
//1.huoqu
struct msgbuf sendBuf = {888,"this is message from quen"};
struct msgbuf readBuf;
memset(&readBuf,0,sizeof(struct msgbuf));
key_t key;
key = ftok(".",'m');
printf("key=%x\n",key);
int msgId = msgget(key, IPC_CREAT|0777);
if(msgId == -1 ){
printf("get que failuer\n");
}
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
//为什么这里传sendbuf的地址,但是第三个参数传数据长度的时候却只传sendBuf.mtext的长度,而不是整个结构体的长度?
//GPT回答:首先sizeof(sendBuf)包含了mtype和mtext的大小,但msgsnd()函数只需要消息内容的大小,所以用strlen(sendBuf.mtext)。mtype不需要传入msgsnd()函数的参数中,因为mtype(数据类型)的作用只是:消息队列是根据mtype来进行消息分类和筛选的,msgsnd()函数在发送消息时会根据mtype进行筛选匹配。
//我简而言之:因为msgsnd函数中封装好了它做了处理,我们传&sendBuf地址给它,msgsnd函数内部会根据sendBuf.mtype进行消息分类和匹配,然后msgsnd函数内部会以sendBuf.mtext为我们需要传送的数据为长度,我们也只需要发送这个数据
printf("send over\n");
msgrcv(msgId, &readBuf,sizeof(readBuf.mtext),988,0);
printf("reaturn from get:%s\n",readBuf.mtext);
msgctl(msgId,IPC_RMID,NULL);
return 0;
}
三、信号
疑惑点
1.signal函数的原型:
按照下图的紫色字体,一步一步拆解:
四、线程
4.1 什么是线程?
线程是操作系统能够进行运算调度的最小单位,它是进程的一部分,是 CPU 调度和分派的基本单位。
简单来说,线程是程序执行流的最小单位,一个标准的线程有自己的寄存器空间和栈空间,但共享进程的资源(如内存、文件句柄等)。
4.11 形象举例
想象一下,你正在一家餐厅工作,这家餐厅有多个服务员(进程),每个服务员负责接待不同的客人(任务)。现在,每个服务员(进程)可以同时处理多桌客人的订单和服务请求(线程)。
场景描述
-
餐厅(操作系统):餐厅相当于操作系统或进程集合。
餐厅是一个大的环境,提供了服务的场所,类似于操作系统提供了一个运行程序的环境。餐厅可以看作是多个进程的集合,每个服务员(进程)都在这个环境中运作。 -
服务员(进程):每个服务员相当于一个进程。
每个服务员独立工作,可以处理不同的任务请求。
进程也是独立的执行单元,可以创建多个线程来处理不同的任务。 -
处理订单(线程):每个桌子的订单处理就是一个线程。
处理订单是服务员的一项具体任务,可以并发执行多个订单。
线程是进程中的一个具体任务,也可以并发执行多个任务。 -
餐具、菜单(共享资源):餐具、菜单相当于共享资源 或 全局变量。
餐具和菜单(共享资源)是餐厅内所有服务员(进程)都可以使用的资源。 -
同步机制:为了确保这些共享资源在多线程环境下正确访问,通常需要同步机制来防止竞态条件(race conditions)。例如:
- 互斥锁:用来保护共享资源,确保同一时间只有一个线程可以访问。
- 信号量:用来管理资源的可用性。
- 条件变量:用来协调线程之间的操作。
服务员 A 可以同时处理桌子 1 和桌子 2 的订单。
服务员 B 可以同时处理桌子 3 和桌子 4 和桌子5 的订单。
每个服务员(进程)有自己的状态和资源,但共享餐厅的资源(如餐具、菜单等)。
4.2 常用的线程函数
- ①
pthread_create
:创建一个线程。 - ②
pthread_exit
:结束一个线程。 - ③
pthread_join
:等待一个线程执行完毕。 - ④
pthread_self
:获取当前线程的ID。 - ⑤
pthread_detach
:分离线程,使线程在结束时自动释放资源。
4.3 如何使用线程?
在C语言中,通常使用POSIX线程库(pthread
)来管理线程。pthread
提供了一系列函数来创建、控制、同步线程。使用线程的流程通常如下:
- ① 创建线程:使用
pthread_create
函数生成一个新的线程。 - ② 线程任务:线程执行的任务由一个函数来定义。
- ③ 等待线程完成:使用
pthread_join
等待线程执行完毕,确保主程序在所有线程结束后才退出。 - ④ 线程退出:用
pthread_exit
结束线程任务。
示例代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// ②线程1的任务:打印数字
void* print_numbers(void* arg) {
for (int i = 1; i <= 5; i++) {
printf("线程1:打印数字 %d\n", i);
sleep(1); // 模拟耗时操作
}
return NULL;
}
// ②线程2的任务:打印字母
void* print_letters(void* arg) {
for (char letter = 'A'; letter <= 'E'; letter++) {
printf("线程2:打印字母 %c\n", letter);
sleep(1); // 模拟耗时操作
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// ①创建线程
pthread_create(&thread1, NULL, print_numbers, NULL);
pthread_create(&thread2, NULL, print_letters, NULL);
// ③等待线程结束,之后再往下执行结束主函数。
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("所有线程完成任务\n");
return 0;
}
4.5 线程的特点
-
轻量级:
- 线程比进程更轻量级,因为它们共享进程的资源,不需要单独的地址空间。
-
并发执行:
- 线程可以并发执行,允许多个线程同时运行,提高程序的执行效率。
-
通信方便:
- 线程之间可以直接访问同一进程内的数据,无需复杂的通信机制。
-
资源共享:
- 同一进程内的线程共享内存、文件句柄等资源。
4.6 总结
线程是进程中的一个执行单位,可以并发执行任务,提高程序的执行效率。通过使用线程,可以实现多任务的同时处理,使得程序更加高效和响应迅速。
4.7 函数原型(疑惑点)
1.线程的创建
#include <pthread.h>
/**
* 创建一个新线程
*
* pthread_t *thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符
* const pthread_attr_t *attr: pthead_attr_t结构体,这个参数可以用来设置线程的属性,如优先级、栈大小等。如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。
* void *(*start_routine)(void *): 一个指向函数的指针,它定义了新线程开始执行时的入口点。这个函数必须接受一个 void * 类型的参数,并返回 void * 类型的结果
* void *arg: start_routine 函数的参数,可以是一个指向任意类型数据的指针
* return: int 线程创建结果
* 成功 0
* 失败 非0
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);
1.1 int pthread_create函数每个参数的含义
int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
①pthread_t *thread
1.每个线程都有一个唯一的标识符(即线程ID),这个标识符是通过pthread_t类型的变量来表示的,当pthread_create成功创建一个线程时,它会将新线程的标识符存储在thread参数指向的位置。
2.pthread_t 定义在头文件<pthreadtypes.h>中,实际上是long类型(long和long int是相同类型的不同写法)的别名。
typedef unsigned long int pthread_t;
②const pthread_attr_t *attr
pthead_attr_t 结构体,这个参数可以用来设置线程的属性,如优先级、栈大小等。 如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。
③void * (* start_routine) (void * )
`void *(*start_routine)(void *)` 是一个指向函数的指针,
该函数接受一个 `void *` 类型的参数,并返回一个 `void *` 类型的值。
这种类型的指针通常用于需要回调函数或多态性的场景,比如在多线程编程中定义线程的启动函数。
1.这个参数中的每个*的含义。
-
(*start_routine)
:定义了一个指向函数的指针 start_routine,指针变量start_routine存着那个函数的地址(*)
: 这个星号和括号表示这是一个指针。在这里,它表示我们正在定义一个指针。start_routine
: 这是变量名,表示我们要定义的指针变量的名字。
-
void *
:这表示start_routine
指向的函数返回一个void *
类型的值。也就是说,这个函数返回一个指向任意类型的指针。 -
(void *)
: 这表示start_routine
指向的函数接受一个void *
类型的参数。也就是说,这个函数可以接受任何类型的指针作为参数。 -
综合起来
void *(*start_routine)(void *)
定义了一个指向函数的指针start_routine
,这个函数接受一个void *
类型的参数,并返回一个void *
类型的值。
2.其实函数名是一种指针,它指向函数的起始地址。
所以 void*(* start_routine) (void * )就相当于void* start_routine (void * )
当我们通过函数名调用函数时,程序实际上是通过这个地址找到并执行函数的代码。因此,可以说函数名是一种指针,它指向函数的起始地址。这种特性使得我们可以将函数名赋值给一个指针变量,即函数指针,通过这个函数指针来间接调用函数。
④void *arg
void * arg是 pthread_create函数的 参数 void*(* start_routine) (void * ) 的参数;
void * arg 对应 void*(* start_routine) (void * ) 的 (void * ),
void * arg 传入的值给 void*(* start_routine) (void * ) 的 (void * ),
void * arg可以是一个指向任意类型数据的指针。
1.2 int pthread_create函数的返回值return
- return: int 线程创建的返回值(结果)
-
成功 0
-
失败 非0
2.线程的退出
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
- 线程只是从启动例程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit
2.1 pthread_exit
#include <pthread.h>
int pthread_exit(void *rval_ptr);
rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。
4.8 互斥锁
死锁
前提条件:至少有两把锁
一开始,线程1手里拿着a锁,线程2手里拿着b锁;
然后呢,线程1又想去拿到b锁,线程2又想拿到a锁;
两个线程互相都想拿到对方手里的锁
,但是又拿不到,所以两个线程就一直卡拿锁那一步。导致死锁。
五.网络编程
1.字节序
Little endian 小端字节序 :把数据的低位先放在内存的中起始位置
Big endian 大端字节序 :把数据的高位先放在内存的中起始位置
一般网络传输时的字节序 =大端字节序
x86系列CPU都是 = 小端字节序.
1.1 字节序转换api
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值
2.Socket服务器和客户端的开发步骤
如上图,TCP服务器和TCP客户端连接的过程如下:
①首先TCP服务器这边,先调用socket()来创建套接字。
套接字这个术语的由来可以追溯到英文单词"socket",意思是 “插座"或"接口” 。在计算机网络编程中,套接字就像是一种插座,它提供了通信的接口,使得不同计算机之间能够进行数据传输和通信。
②调用bind() 给套接字绑定IP地址和端口号
③调用listen() 监听网络连接,等别人来连
————————————
④TCP客户端这边调用socket()来创建套接字
⑤如图红色线,因为TCP客户端知道TCP服务器的IP地址和端口号,所以TCP客户端直接调用connect() 去连接TCP服务器
————————————
⑥TCP服务器监听到有客户端申请接入,调用accept() 接受这个客户端的连接
————————————
⑦TCP客户端和TCP服务端,进行数据的交互(互相读read(),写write() )
⑧关闭套接字,断开连接
3.Socket连接使用的API详解
【服务器部分】— — — — — — — — — — — — — — — —
3.1 int socket() —— 指定讲“汉语”(连接协议TCP/UDP)
int socket (int domain, int type, int protocol);3.2 int bind() —— 地址准备好(我ip地址(楼号)是。我端口号(房间号)是。。)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);3.2.1 端口号需要用到 字节序转换API(看1.1)
因为这里的端口号会传到网络上去,
我们现在主机是x86系列CPU,都是小端字节序
而网络传输用的是大端字节序
所以需要使用字节序转换API来转换一下。
3.2.2 地址转换API
int inet_aton(const char* straddr,struct in_addr *addrp);把字符串形式的"192.168.1.123"转为网络传输能识别的格式
例:int inet_aton(“192.168.1.123”, &s_addr.sin_addr);
此时变量s_addr.sin_addr的值就是网络能识别的格式。
把网络格式的ip地址转为字符串形式(我们能看得懂的形式)
3.3 listen() —— 监听(等待大家的来访,等别人敲门)
int listen(int sockfd, int backlog);3.4 accept() —— 接受连接(同意别人进来房间)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
出错的时候返回值是-1,
成功的时候返回值是新的套接字描述符
3.5 数据收发
ssize_t write(int fd, const void*buf, size_t nbytes); ssize_t read(int fd, void *buf, size_t nbyte);3.6 数据收发常用的第二套API(就比上面第一套的多一个flags参数,flags主要用来控制是否有阻塞等等。其它前面三个参数是一模一样)
ssize_t send(int s, const void *msg, size_t len, int flags); ssize_t recv(int s, void *buf, size_t len, int flags);【客户端部分】— — — — — — — — — — — — — — — —
3.7 客户端创建套接字socket()
3.8 客户端的connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);4.代码——Socket服务器和客户端的开发代码
4.1 Socket服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
//chenlichen
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
int mark = 0;
char msg[128] = {0};
// char *msg = "I get your connect";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
//2. bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3. listen
listen(s_fd,10);
//4. accept
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
mark++;
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){
if(fork()==0){
while(1){
sprintf(msg,"welcom No.%d client",mark);
write(c_fd,msg,strlen(msg));
sleep(3);
}
}
//5. read
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else if(n_read>0){
printf("\nget: %d\n",n_read);
}else{
printf("client quit\n");
break;
}
}
break;
}
}
return 0;
}
4.2 Socket客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
int tmp;
// char *msg = "msg from client";
char msg[128] = {0};
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
printf("%d\n",getpid());
//1. socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//2.connect
if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(-1);
}
while(1){
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input: ");
gets(msg);
write(c_fd,msg,strlen(msg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("\nget:%s\n",readBuf);
}
}
}
//3.send
//4.read
return 0;
}