【Linux】系统编程、网络编程

Linux系统编程、网络编程


前言

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 形象举例

想象一下,你正在一家餐厅工作,这家餐厅有多个服务员(进程),每个服务员负责接待不同的客人(任务)。现在,每个服务员(进程)可以同时处理多桌客人的订单和服务请求(线程)。

场景描述
  1. 餐厅(操作系统):餐厅相当于操作系统或进程集合。
    餐厅是一个大的环境,提供了服务的场所,类似于操作系统提供了一个运行程序的环境。餐厅可以看作是多个进程的集合,每个服务员(进程)都在这个环境中运作。

  2. 服务员(进程):每个服务员相当于一个进程。
    每个服务员独立工作,可以处理不同的任务请求。
    进程也是独立的执行单元,可以创建多个线程来处理不同的任务。

  3. 处理订单(线程):每个桌子的订单处理就是一个线程。
    处理订单是服务员的一项具体任务,可以并发执行多个订单。
    线程是进程中的一个具体任务,也可以并发执行多个任务。

  4. 餐具、菜单(共享资源):餐具、菜单相当于共享资源 或 全局变量。
    餐具和菜单(共享资源)是餐厅内所有服务员(进程)都可以使用的资源。

  5. 同步机制:为了确保这些共享资源在多线程环境下正确访问,通常需要同步机制来防止竞态条件(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 线程的特点

  1. 轻量级

    • 线程比进程更轻量级,因为它们共享进程的资源,不需要单独的地址空间。
  2. 并发执行

    • 线程可以并发执行,允许多个线程同时运行,提高程序的执行效率。
  3. 通信方便

    • 线程之间可以直接访问同一进程内的数据,无需复杂的通信机制。
  4. 资源共享

    • 同一进程内的线程共享内存、文件句柄等资源。

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); //返回主机字节序的值

这里的h代表host主机,n代表net网络,s代表short(两个字节),l代表long(4个字节) 以第一条api为例:htons的意思就是h(host) to n(net) s(net),主机 to转换成 网络字节序 短型

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的值就是网络能识别的格式。

char* inet_ntoa(struct in_addr inaddr);

网络格式的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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值