注:本文系湛江市岭南师范学院物联网俱乐部原创部分训练项目,转载请保留声明。
前言
?今天是闭关的第25天,今天给大家带来Linux的多线程编程的博客,这与前面的进程会有点类似,但是进程与线程是有很大的区分的,请大家好好学习。
一、线程
1.1 线程的定义
??线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
1.2 进程与线程的区别
a)进程 ??进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元; ??进程是程序执行时的一个实例,即它是程序已经执行到某种程度的数据结构的汇集。 ??从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。 ??Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响。
??Linux中的进程包含3个段,分别为“数据段”、“代码段”和“堆栈段”。
???“数据段”存放的是全局变量、常数以及动态数据分配的数据空间;
???“代码段”存放的是程序代码的数据。
???“堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。
? b)线程 ??线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
? c)比较 ①"进程——资源分配的最小单位,线程——程序执行的最小单位"
②进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。
③线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个主线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
④进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。
? d)线程优点 ①使用多线程的理由之一:
??和进程相比,它是一种非常“节俭”的多任务操作方式.在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。
??运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需的时间也远远小于进程间切换所需要的时间.据统计,一个进程的开销大约是一个线程开销的30倍左右。
②使用多线程的理由之二:
??线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
??多线程程序作为一种多任务、并发的工作方式,还有如下优点:使多CPU系统更加有效.操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上.改善程序结构.一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改.
e)使用条件 ①一般来说,要跳转程序就用多进程编程,fork()+execl() ②一般来说,项目开发时,如果需要资源共享和节约资源,尽可能用多线程。
1.3 守护进程
daemon进程 ??Unix/Linux中的守护进程(Daemon)类似于Windows中的后台服务进程,一直在后台长时间运行的进程。它通常在系统启动后就运行,没有控制终端,也无法和前台的用户交互,在系统关闭时才结束。Daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的Daemon程序称作守护进程。比如的网络服务程序,可以在完成创建套接口,绑定套接口,设置套接口为监听模式后,变成守护进程进入后台执行而不占用控制终端,这是网络服务程序的常用模式。UNIX下的网络服务程 序,如WebServer(Nginx、Apache等),FTP(vsftp),SSH(openssh)等一般都是由守护进程(Daemon)来实现的。守护进程不占用终端,在后台运行。UNIX的守护进程一般都命名为 *d 的形式,如httpd,vsftpd,sshd等。守护进程一旦脱离了终端,退出就成了问题,这时需要使用 ps 命令查出进程ID然后再使用kill命令停止。 ??daemon进程和普通进程不一样吗?为什么要单独提出如何编写daemon进程呢?有时在Linux上面打开一个命令行终端,输入编译命令进行编译,编译的时间可能比较长,这时候你不小心关闭了这个terminal,编译就中断了。因为编译脚本是作为当前终端的一个子进程来执行的,当终端退出后子进程也就退出了。在学完Linux基本命令使用之后我们知道,在命令后面加上 后台运行符(&) 可以让该命令在后台运行。如:
make &
??该命令会让编译命令make到后台执行,这样只是造成了make在后台一直运行的假象,它依然没有脱离和terminal之间的父子关系。当terminal退出后,make依然会退出。而作为daemon进程,我们希望一旦启动就能在后台一直运行,不会随着terminal的退出而结束,所以针对daemon进程就要用特殊的编程来处理。 ??Linux系统专门提供了一个用来创建daemon进程的库函数,该函数的原型是:
#include <unistd.h>
int daemon(int nochdir, int noclose);
??其中参数 nochdir 指定是否要切换当前工作路径到"/“根目录,而参数noclose 指定是否要关闭标准输入、标准输出和标准出错(即重定向到/dev/null)。在创建守护进程的时候,往往需要将进程的工作目录修改为”/"根目录,并将标准输入、标准输出和标准出错关闭。所以这两个参数我们一般都是传0。
1.4 系统日志
syslog ??对于在后台默默运行的守护进程,本身不应该往标准输出或标准出错上输出程序运行的任何状态信息,我们也一般会关闭三个标准I/O,那么程序的运行状态信息该如何查看呢?当然我们可以自己写函数把程序运行的而相关信息记录到文件中,另外一种方式就是使用Linux系统自带的syslog日志机制。syslog是一种工业标准的协议,可用来记录设备的日志。在UNIX系统,路由器、交换机等网络设备中,系统日志(System Log)记录系统中任何时间发生的大小事件。管理者可以通过查看系统记录,随时掌握系统状况。UNIX的系统日志是通过syslogd这个进程记录系统有关事件记录,也可以记录应用程序运作事件。通过适当的配置,我们还可以实现运行syslog协议的机器间通信,通过分析这些网络行为日志,藉以追踪掌握与设备和网络有关的状况。
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
函数说明:打开日志设备,以供读取和写入,与文件系统调用的open类似;调用openlog是可选择的。如果不调用openlog,则在第一次
调用syslog时,自动调用openlog。
参数说明:
ident:是一个标记,ident 所表示的字符串将固定的加在每行日志的前面一标识这个日志,通常就写成当前程序的名称以
作标记。
option: 指定openlog函数和接下来调用的syslog函数的控制标志。可以取以下值:
LOG_CONS 如果将信息发送给 syslogd 守护进程时发生错误,直接将相关信息输出到终端
LOG_NDELAY 立即打开与系统日志的连接(通常情况下,只有在产生第一条日志信息的情况下才会打开与日志系统
的连接)
LOG_ODELAY 类似于 LOG_NDELAY 参数,与系统日志的连接只有在 syslog 函数调用时才会创建
LOG_PERROR 在将信息写入日志的同时,将信息发送到标准错误输出
LOG_PID 每条日志信息中都包含进程号
facility:指定记录消息程序的类型,与 syslogd 守护进程的配置文件 syslog.conf 中的 facility 对应。可取如下
值:
LOG_AUTH 认证系统(login、su、getty等)
LOG_AUTHPRIV 同 LOG_AUTH 但只登陆到所选择的单个用户可读的文件中。
LOG_CRON cron 守护进程
LOG_DAEMON 其他系统守护进程,如 routed
LOG_FTP 文件传输协议:ftpd、tftpd
LOG_KERN 内核产生的消息
LOG_LPR 系统打印机缓冲池:lpr、lpd
LOG_MAIL 电子邮件系统
LOG_NEWS 网络新闻系统
LOG_SYSLOG 由 syslogd(8)产生的内部消息
LOG_USER 随机用户进程产生的消息
LOG_UUCP UUCP 子系统
LOG_LOCAL0 ~ LOG_LOCAL7 本地使用保留
void syslog(int priority, const char *format, ...);
函数说明:写入日志,与文件系统调用 printf使用方法类似,但在前面指定日志级别。
参数说明:
priority:表示消息的级别,与 syslogd 守护进程的配置文件 syslog.conf 中的 level 对应。可取如下值:
LOG_EMERG 紧急情况
LOG_ALERT 应该被立即改正的问题,如系统数据库破坏
LOG_CRIT 重要情况,如硬盘错误
LOG_ERR 错误
LOG_WARNING 警告信息
LOG_NOTICE 不是错误情况,但是可能需要处理
LOG_INFO 情报错误
LOG_DEBUG 包含情报的信息,通常指在调试一个程序时使用
void closelog(void);
函数说明:关闭日志设备,与文件系统调用的close类似;调用closelog也是可选择的,它只是关闭被用于与syslog守护进程通信的描述
符。
??所有程序(包括Linux内核) 调用syslog()函数的输出相关信息都会记录到 /var/log/messages 日志文件中,因为该文件中记录了所有的信息,这也意味着当前程序记录的消息容易被别的程序冲刷掉,所以我们在做项目写网络程序的时候,一般会使用标准文件IO库(fopen、fwrite()等)自己实现日志系统,而不是直接调用该函数。
1.5 信号
??在上面的例程中,我们可以看到当使用 kill 命令杀死程序时,第28行到第31行并不会被执行。这是因为kill命令默认会让程序直接终止,而不是让while(1) 死循环停止。如果我们想让程序“优雅”地退出,也就是在程序退出之前还想做某些事则需要了解Linux下的信号机制。 ??软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill()发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。 收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类: ? ?1. 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。 ?2. 第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。 ?3. 第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通 过系统调用signal来指定进程对某个信号的处理行为。
??Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源,它的主要问题是信号可能丢失。随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。我们可以使用 kill -l 命令查看当前系统支持的信号,需要注意的是不同的系 统支持的信号是不一样的: ??信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
1.6 多线程编程
??在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。 ??线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
1.7 互斥锁
?试想一下,我们寝室/实验室只有一个洗手间,那多个人是怎么解决马桶共享的问题?对,那就是锁的机制!在这里马桶就是临界资源,我们在进入到洗手间(临界区)后,就首先上锁; 然后用完离开洗手间(临界区)之后,把锁释放供别人使用。如果有人想去洗手间时发现门锁上了,他也有两种策略:1,在洗手间那里等(阻塞); 2,暂时先离开等会再过来看(非阻塞); ?死锁:
参考链接: ①互斥和同步概述 互斥锁 链接:https://blog.csdn.net/qq_44226094/article/details/105165767
②Linux—死锁及避免死锁的方法 链接:https://blog.csdn.net/qq_37934101/article/details/81869245
③学习笔记 c++ (linux pthread C++ 多线程互斥锁) 链接:https://blog.csdn.net/qq_42145185/article/details/82868817
④输出所在的函数的函数名 链接:https://zhidao.baidu.com/question/259085447.html
1.8 线程API函数
Linux下的多线程遵从POSIX线程接口,简称pthread,在pthread库中提供,具体用法可以百度,也可以man手册。
pthread_create():创建一个线程
pthread_exit():退出一个线程
pthread_jion():阻塞当前线程,直到另一个线程执行结束
pthread_attr_init():设置线程是否脱离属性
pthread_kill():给线程发送kill信号
同步函数:
pthread_mutex_lock():互斥加锁
pthread_mutex_unlock():互斥锁解锁
pthread_cond_init():初始化条件变量
pthread_cond_signal():发送信号唤醒进程
pthread_cond_wait():等待条件变量的特殊事件发生
二、源码与效果图展示
2.1 源码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
#define BACKLOG 13
typedef void *(THREAD_BODY)(void *thread_arg);
void *thread_worker(void *ctx);
int thread_start(pthread_t *thread_id, THREAD_BODY *thread_workbody, void *thread_arg);
int main(int argc, char **argv)
{
int listen_fd, client_fd = -1;
struct sockaddr_in serv_addr;
struct sockaddr_in cli_addr;
socklen_t cliadd_len = sizeof(cli_addr);
pthread_t tid;
int listen_port;
if (argc < 2)
{
printf("Program usage:%s [Port]\n", argv[0]);
return -1;
}
listen_port = atoi(argv[1]);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
{
printf("create socket failure:%s\n", strerror(errno));
printf("listen_fd:%d\n", listen_fd);
return -1;
}
printf("create socket success!\n");
printf("socket create fd[%d]\n", listen_fd);
printf("\n\n");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(listen_port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("bind socket failure:%s\n", strerror(errno));
return -2;
}
printf("bind socket success!\n");
printf("socket[%d] bind on port[%d] for all ip address ok\n", listen_fd, listen_port);
printf("\n\n");
listen(listen_fd, BACKLOG);
while (1)
{
printf("\nStart waiting and accept new client connect..\n");
client_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cliadd_len);
if (client_fd < 0)
{
printf("accept new socket failure:%s\n", strerror(errno));
return -2;
}
printf("accept new socket success!\n");
printf("Accept new client[%s:%d] with fd[%d]\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), client_fd);
printf("\n\n");
thread_start(&tid, thread_worker, (void *)client_fd);
}
close(listen_fd);
}
int thread_start(pthread_t *thread_id, THREAD_BODY *thread_workbody, void *thread_arg)
{
int rv = -1;
pthread_attr_t thread_attr;
if (pthread_attr_init(&thread_attr))
{
printf("pthread_attr_init() failure :%s\n", strerror(errno));
goto CleanUp;
}
if (pthread_attr_setstacksize(&thread_attr, 120 * 1024))
{
printf("pthread_attr_setstacksize failure:%s\n", strerror(errno));
goto CleanUp;
}
if (pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED))
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
goto CleanUp;
}
if (pthread_create(thread_id, &thread_attr, thread_workbody, thread_arg))
{
printf("Create thread failure:%s\n", strerror(errno));
goto CleanUp;
}
rv = 0;
CleanUp:
pthread_attr_destroy(&thread_attr);
return rv;
}
void *thread_worker(void *ctx)
{
int client_fd;
int rv;
char buff[1024];
int i;
if (!ctx)
{
printf("Invaild input arguments in %s\n", __FUNCTION__);
pthread_exit(NULL);
}
client_fd = (int)ctx;
printf("Child thread start to communicate with socket client\n");
while (1)
{
memset(buff, 0, sizeof(buff));
rv = read(client_fd, buff, sizeof(buff));
if (rv < 0)
{
printf("Read data from client socket[%d] failure:%s\n", client_fd, strerror(errno));
printf("\n\n");
close(client_fd);
pthread_exit(NULL);
}
else if (rv == 0)
{
printf("client socket[%d] disconect\n", client_fd);
printf("\n\n");
close(client_fd);
pthread_exit(NULL);
}
printf("Read %d bytes data from client[%d] and echo it back: '%s'\n", rv, client_fd, buff);
printf("\n\n");
for (i = 0; i < rv; i++)
{
buff[i] = toupper(buff[i]);
}
if (write(client_fd, buff, rv) < 0)
{
printf("write %d bytes data back to client[%d] failure:%s\n", rv, client_fd, strerror(errno));
printf("\n\n");
close(client_fd);
pthread_exit(NULL);
}
printf("write %d bytes data back to client[%d]\n", rv, client_fd);
printf("\n\n");
sleep(1);
}
}
2.2 效果图
(1)客户端连接配置图: (2)服务器连接图,打印客户端IP地址: (3)服务器接收客户端的信息图:
三、总结
多线程,我曾经也看不懂,不过经过此次特训以后,觉得还好,接下来会更难,我们将会学到多路复用select(),poll()和epoll()。
|