【嵌入式Linux笔记】第三篇:Linux应用开发基础(下)<待更新>

文章目录

前言

六、网络编程

1. 网络通信概述

2. TCP编程

3. UDP编程

七、多线程编程

1. 线程的使用

2. 线程的等待与唤醒

3. 基于互斥量的线程互斥

八、I2C应用编程

1. I2C协议简介

2. SMBus协议


前言

由于知识内容较多,Linux应用开发基础共分为上、下两篇。此篇为下篇,承接【嵌入式Linux笔记】第三篇:Linux应用开发基础(上)。若存在版权问题,请联系删除。


六、网络编程

1. 网络通信概述

1.1 背景:在多个设备存在的情况下,若采用串口的方式实现多设备通信,将会导致管理困难和通信效率低等问题。因此,网络通信横空出世,能够实现客户端与服务器之间的高效率通信。

1.2 数据传输三要素:源、目的、长度。在网络通信前,需要指定通信的源、目的地以及发送数据的长度大小。使用“ IP 和端口”来表示源或目的。

1.3 网络通信对象及传输方式:如下图所示,当我们访问网站时,通信过程涉及客户端(client)服务器(server)这两个对象。而二者的传输通过TCP/UDP协议。

2. TCP编程

2.1 TCP网络通信交互图:

  • 服务器:①服务器利用socket函数返回一个文件描述符fd。使用bind函数将服务器的fd、IP和端口绑定在一起。调用listen函数启动检测有无新客户端的连接请求。采用accept函数来接受客户端发起的连接请求。调用send和recv函数来发送和接收数据。使用close函数来关闭该连接。
  • 客户端:①使用socket函数返回一个文件描述符fd。调用connect函数向服务器发起连接请求。调用send和recv函数来发送和接收数据。使用close函数来关闭该连接。

2.2 多客户端单服务器实验:

(1) 目标:实现多个客户端给服务器发送数据,服务器接收数据并显示数据。

(2) 思路:server.c和client.c分别实现服务器和客户端的程序代码。

(3) server.c代码:

#include <stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8888
#define BACKLOG		10

int main(int argc, char* argv[])
{
	int client_num = -1;	//记录客户端连接的数量
	signal(SIGCHLD,SIG_IGN);//避免客户端退出造成server子进程僵死
	/* 1.socket */
	int iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
	if (iSocketServer == -1)
	{
		printf("socket error!\n");
		return -1;
	}

	/* 2.bind */
	struct sockaddr_in tSocketServerAddr;
	tSocketServerAddr.sin_family = AF_INET;			//使用Internet一般为AF_INET
	tSocketServerAddr.sin_port = htons(SERVER_PORT);//将服务器端口转化为网络字节序
	tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;	//表示可以和任何客户端通信
	memset(tSocketServerAddr.sin_zero, 0, 8);			
	int bind_ret = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
	if (bind_ret == -1)
	{
		printf("bind error!\n");
		return -1;
	}

	/* 3. listen */
	int listen_ret = listen(iSocketServer, BACKLOG);//最多监听10个
	if (listen_ret == -1)
	{
		printf("listen error!\n");
		return -1;
	}
	
	while(1)
	{
		/* 4.accept */
		int clientaddr_len = sizeof(struct sockaddr);
		struct sockaddr_in tSocketClientAddr;
		int accept_ret = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &clientaddr_len);
		if (accept_ret == -1)
		{
			printf("accept error!\n");
			return -1;
		}
		else
		{
			client_num++;
			printf("Get connect from %d: %s\n", client_num, inet_ntoa(tSocketClientAddr.sin_addr));
			if (!fork())	//创建子进程,且复制父进程的执行顺序
			{
				/* 5. 服务器循环接收数据 */
				while(1)
				{
					char Recvbuf[1000];
					int recv_ret = recv(accept_ret, Recvbuf, 999, 0);
					if (recv_ret <= 0)
					{
						close(iSocketServer);
						return -1;
					}
					else
					{
						Recvbuf[recv_ret] = '\0';
						printf("Get Msg from %d: %s\n", client_num, Recvbuf);
					}
				}
			}
		}
	}
	/* 6. close */
	close(iSocketServer);
	return 0;
}

(4) client.c代码:

#include <stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8888

int main(int argc, char* argv[])
{
	/* 0.命令行格式 */
	if (argc != 2)
	{
		printf("格式:%s <server_ip>\n", argv[0]);
		return -1;
	}
	
	/* 1.socket */
	int iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
	if (iSocketClient == -1)
	{
		printf("socket error!\n");
		return -1;
	}

	/* 2.connect */
	struct sockaddr_in tSocketServerAddr;
	//2.1 给tSocketServerAddr结构体赋值
	tSocketServerAddr.sin_family = AF_INET;			
	tSocketServerAddr.sin_port = htons(SERVER_PORT);
	if (inet_aton(argv[1], &tSocketServerAddr.sin_addr) == 0)
	{
		printf("invalid server ip!\n");
		return -1;
	}
	memset(tSocketServerAddr.sin_zero, 0, 8);
	//2.2 连接到服务器
	int connect_ret = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
	if (connect_ret == -1)
	{
		printf("connect error!\n");
		return -1;
	}

	/* 3. 键盘获取数据,循环发送数据*/
	char Sendbuf[1000];
	while(1)
	{
		if (fgets(Sendbuf, 999, stdin))
		{
			int send_ret = send(iSocketClient, Sendbuf, strlen(Sendbuf), 0);
			if (send_ret <= 0)
			{
				close(iSocketClient);
				return -1;
			}
		}
	}
	return 0;
}

(5) 执行结果:首先,打开三个命令行终端,第一个作为server,第二个和第三个作为client,结果如下图。两个client连接到server,server都会显示连接的客户端ip。两个client发送数据时,server都能收到并且打印出来。

(6) 说明:

  • 为了防止server子进程退出造成的僵死,需要在server.c中包含signal(SIGCHLD,SIG_IGN);语句。另外,可以使用命令"ps -a"来查看进程。
  • 无论是server.c还是client.c,都要注意send和recv函数的返回值,这涉及到程序退出。server.c中的recv_ret<=0包含了接收错误和接收0字节(即客户端退出)两种情况。client.c中的send_ret<=0包含了发送错误和发送0字节(即退出)两种情况。

3. UDP编程

 3.1 UDP网络通信交互图:

  • 服务器:①服务器利用socket函数返回一个文件描述符fd(注意使用SOCK_DGRAM选项)。使用bind函数将服务器的fd、IP和端口绑定在一起。调用sendto和recvfrom函数来发送和接收数据。使用close函数来关闭该连接。
  • 客户端:①使用socket函数返回一个文件描述符fd(注意使用SOCK_DGRAM选项)。调用sendto和recvfrom函数来发送和接收数据。使用close函数来关闭该连接。

3.2 多客户端单服务器实验:与TCP类似!

(1) 目标:实现多个客户端给服务器发送数据,服务器接收数据并显示数据。

(2) 思路:server.c和client.c分别实现服务器和客户端的程序代码。

(3) server.c代码:

#include <stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8888
#define BACKLOG		10

int main(int argc, char* argv[])
{
	/* 1.socket */
	int iSocketServer = socket(AF_INET, SOCK_DGRAM, 0);
	if (iSocketServer == -1)
	{
		printf("socket error!\n");
		return -1;
	}

	/* 2.bind */
	struct sockaddr_in tSocketServerAddr;
	tSocketServerAddr.sin_family = AF_INET;			//使用Internet一般为AF_INET
	tSocketServerAddr.sin_port = htons(SERVER_PORT);//将服务器端口转化为网络字节序
	tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;	//表示可以和任何客户端通信
	memset(tSocketServerAddr.sin_zero, 0, 8);			
	int bind_ret = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
	if (bind_ret == -1)
	{
		printf("bind error!\n");
		return -1;
	}

	/* 3.recvfrom */
	while(1)
	{
		char Recvbuf[1000];
		struct sockaddr_in tSocketClientAddr;
		int addrlen = sizeof(struct sockaddr);
		int recv_ret = recvfrom(iSocketServer, Recvbuf, 999, 0, (struct sockaddr*)&tSocketClientAddr, &addrlen);
		if (recv_ret > 0)
		{
			Recvbuf[recv_ret] = '\0';
			printf("Get Msg from %s: %s\n", inet_ntoa(tSocketClientAddr.sin_addr), Recvbuf);
		}
	}
	/* 4. close */
	close(iSocketServer);
	return 0;
}

(4) client.c代码:

#include <stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8888

int main(int argc, char* argv[])
{
	/* 0.命令行格式 */
	if (argc != 2)
	{
		printf("格式:%s <server_ip>\n", argv[0]);
		return -1;
	}
	
	/* 1.socket */
	int iSocketClient = socket(AF_INET, SOCK_DGRAM, 0);
	if (iSocketClient == -1)
	{
		printf("socket error!\n");
		return -1;
	}

	/* 2.指明server的ip与端口:初始化tSocketServerAddr结构体 */
	struct sockaddr_in tSocketServerAddr;
	tSocketServerAddr.sin_family = AF_INET;			
	tSocketServerAddr.sin_port = htons(SERVER_PORT);
	if (inet_aton(argv[1], &tSocketServerAddr.sin_addr) == 0)
	{
		printf("invalid server ip!\n");
		return -1;
	}
	memset(tSocketServerAddr.sin_zero, 0, 8);

	/* 3. 键盘获取数据,循环发送数据*/
	char Sendbuf[1000];
	while(1)
	{
		if (fgets(Sendbuf, 999, stdin))
		{
			int send_ret = sendto(iSocketClient, Sendbuf, strlen(Sendbuf), 0, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
			if (send_ret <= 0)
			{
				close(iSocketClient);
				return -1;
			}
		}
	}
	return 0;
}

(5) 执行结果:首先,打开三个命令行终端,第一个作为server,第二个和第三个作为client,结果如下图。两个client发送数据时,server都能收到并且打印出来。

(6) 说明:

  • 程序与TCP类似,但socket函数调用时记得采用SOCK_DGRAM选项。
  • client.c中的connect函数可用可不用,用的话与send函数搭配;不用的话,直接使用sendto函数。

七、多线程编程

1. 线程的使用

更多线程的使用详见百问网手册。

1.1 背景:实际程序中,可能存在一边打游戏一遍听歌的需求,若将两种子功能程序写到一个while循环中,可能会造成打游戏的时候无法听歌,听歌的时候无法打游戏,从而降低用户体验。因此,有必要使用多线程,线程1单独处理打游戏,线程2单独处理听歌,可以完美解决上述问题。

1.2 什么是线程

  • 线程是操作系统调度的最小单元。
  • 普通进程中,只有一个线程执行对应的逻辑。
  • 使用多线程编程时,在一个进程中有多个线程执行不同任务。
  • 与多进程编程相比,单个进程中的多个线程之间共享该进程的资源(进程中的全局变量)。

1.3 线程的标识

(1) 每一个进程都有一个唯一对应的PID号来表示该进程,而对于线程而言,也有一个“类似于进程的PID号”,名为tid,其本质是一个pthread_t类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。

(2) pthread_t pthread_self(void)函数:返回当前线程的线程号。

(3) 打印tid例程代码:若gcc编译不通过,可以添加-lpthread库选项。

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

int main(int argc, char* argv[])
{
	pthread_t tid = pthread_self();
	printf("tid = %lu\n", (unsigned long)tid);
	return 0;
}

1.4 线程的创建:调用pthread_create函数

(1) 函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

(2) 参数说明:

第一个参数用来保存新建线程的线程号
第二个参数表示线程的属性,一般传入 NULL 表示默认属性
第三个参数函数指针,就是线程执行函数的地址。这个函数返回值为 void*,形参为 void*
第四个参数表示向线程执行函数传入的参数,若不传入,可用NULL填充

(3) 例程代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* mythread_fun(void* arg)
{
	// 打印新创建线程的tid
	printf("tid_new = %lu\n", (unsigned long)pthread_self());
}

int main(int argc, char* argv[])
{
	/* 创建一个新线程 */
	pthread_t tid;
	int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL);
	if (create_ret)
	{
		printf("pthread create error\n");
		return -1;
	}
	
	/* 打印两个线程号 */
	printf("tid_main = %lu\n", (unsigned long)pthread_self());
	sleep(1);//以防主线程结束了新线程还没创建完
	return 0;
}

2. 线程的等待与唤醒

2.1 二进制信号量:一般常用于保护一段代码,使其每次只被一个执行线程运行。

  • 初始化信号量函数:int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 第一个参数是信号量对象;第二个参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享;第三个参数为sem的初始值。返回值:调用成功时返回0,失败返回-1

2.2 线程的等待:等待其他线程将其唤醒,使用sem_wait函数。如果信号量的值大于零,则将其减一,否则阻塞进程直到信号量变为非零。

  • 函数原型:int sem_wait(sem_t *sem); 
  • 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1

2.3 线程的唤醒:将信号量的值加1,使用sem_post函数。如果有等待的进程,其中一个将被唤醒。

  • 函数原型:int sem_post(sem_t *sem); 
  • 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1

2.4 信号量销毁:调用sem_destroy函数来销毁信号量。

  • 函数原型:int sem_destroy(sem_t *sem); 
  • 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1

2.5 实验:主线程监测键盘输入的数据,并保存在缓存数组中。新线程将缓存数组中的内容打印出来。

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

char inputBuf[1000];//输入缓存数组
sem_t sem;			//信号量,初始化为0

void* mythread_fun(void* arg)
{
	/* 新线程:输出主线程中保存的数据 */
	while(1)
	{
		sem_wait(&sem);
		printf("tid_new: %lu, Buf: %s\n", (unsigned long)pthread_self(), inputBuf);
	}
}

int main(int argc, char* argv[])
{
	/* 初始化信号量 */
	sem_init(&sem, 0, 0);
	
	/* 创建一个新线程 */
	pthread_t tid;
	int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL);
	if (create_ret)
	{
		printf("pthread create error\n");
		return -1;
	}
	
	/* 主线程:保存键盘输入数据 */
	while(1)
	{
		fgets(inputBuf, 999, stdin);
		sem_post(&sem);
	}
	
	return 0;
}

3. 基于互斥量的线程互斥

3.1 互斥量:使用互斥量来保护共享数据,首先要定义和初始化互斥量。然后是使用互斥量的加锁、解锁来保护共享数据,最后使用完销毁互斥量。

3.2 初始化互斥量:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

3.3 互斥量的加锁和解锁:pthread_mutex_lock函数和pthread_mutex_unlock函数。

3.3 改进2.5实验:由于2.5实验中主线程和新线程可能会在同一时间来操作数组inputBuf,从而导致inputBuf内容错误。为此,我们添加线程互斥代码,防止上述情况出现。注意:引入temp_buf是因为sem_post函数通知新线程之后,可能主线程又会直接抢占互斥锁,无法使新线程打印信息。为此,利用fgets的休眠特性将输入数据保存到temp_buf中。这样,就能避免上述问题。如果在sem_post后加sleep函数也能实现效果,但是这样就降低了程序响应。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>

char inputBuf[1000];//输入缓存数组
sem_t sem;			 //信号量,初始化为0
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//初始化互斥量

void* mythread_fun(void* arg)
{
	/* 新线程:输出主线程中保存的数据 */
	while(1)
	{
		sem_wait(&sem);	//sem为1时才会被唤醒,否则一直休眠
		pthread_mutex_lock(&mutex);
		printf("tid_new: %lu, Buf: %s\n", (unsigned long)pthread_self(), inputBuf);
		pthread_mutex_unlock(&mutex);
	}
}

int main(int argc, char* argv[])
{
	/* 初始化信号量 */
	sem_init(&sem, 0, 0);
	
	/* 创建一个新线程 */
	pthread_t tid;
	int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL);
	if (create_ret)
	{
		printf("pthread create error\n");
		return -1;
	}
	
	/* 主线程:保存键盘输入数据 */
	char temp_buf[1000];//临时缓存数组,输入数据首先保存在此处
	while(1)
	{
		fgets(temp_buf, 999, stdin);//有输入数据时才被唤醒
		pthread_mutex_lock(&mutex);
		memcpy(&inputBuf, &temp_buf, 999);
		pthread_mutex_unlock(&mutex);
		sem_post(&sem);		//sem从0加到1
	}
	
	return 0;
}


八、I2C应用编程

1. I2C协议简介

  • I2C协议内容简述:由SDA数据线和SCL时钟线组成,且主机和从机之间的SDA和SCL均连接在一起,同时这两条线接上拉电阻且配置为开漏输出(为了避免短路情况发生)。主机和从机之间的通信主要通过发送读写数据帧来完成。每一个数据帧由固定的二进制格式组成,此处不详细展开。
  • 注意:①两条线均有可能被主机和从机驱动,当从机忙碌时,可以拉低SCL以执行内部操作。②SCL高电平期间,主/从设备要读取SDA上的数据,因此SCL高电平期间不能修改SDA的值以防读写错误。③I2C发送时序是先发高位再发低位(高位先行)。

2. SMBus协议

SMBus协议是I2C协议的子集,在I2C的基础上定义了更严格的要求,分为硬件要求和软件要求(传输格式),如下图所示。推荐使用SMBus协议,即使I2C中没有给出相关函数,也可以使用软件来模拟SMBus协议。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值