谈谈Linux网络编程

标准I/O

常见标准I/O函数

常用标准I/O库函数总结

fopen

fopen()函数主要用于对文件和终端的输入输出。

#include <stdio.h>
 
FILE *fopen(const char *filename, const char *mode);
  • filaname:指定的文件名,fopen会把它与一个文件流关联起来。

  • mode:指定文件的打开方式:
    “r”/“rb”:只读方式打开
    “w”/“wb”:只写方式打开,把文件长度截为0(你可把它想象为>)
    “a”/“ab”:只写方式打开,新内容追加到文件尾(你可以想象成>>)
    “r+”/“r+b”/“rb+”:以修改方式打开(读写)
    “w+”/“w+b”/“wb+”:以修改方式打开,同时把文件内容截为0
    “a+”/“a+b”/“ab+”:以修改方式打开,新内容追加到文件结尾
    注意:linux会把所有文件都看成是二进制文件,所以那个"b"表示文件是二进制。

返回值:成功时返回一个FILE*指针,失败返回NULL值,NULL定义在stdio.h中。

fread

主要作用是从一个文件流里读取数据,数据从stream读到由ptr指定的数据缓冲区里面。

#include<stdio.h>

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
  • ptr:数据读到ptr指定的缓冲区里面。
  • size:每个数据记录的长度(类似与char,int,long,float之类,指代每次读取块的大小)。
  • nitems:传输的记录个数。
  • stream:指定要读取的数据缓冲区。

返回值:读到缓冲区的记录个数(非字节),如果读到文件尾,其返回值可能小于nitems,甚至可以是0(读取文件为空)。

fwrite

主要从stream获取数据记录写到ptr中,返回值是成功写入的记录个数。

#include<stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t items, FILE *stream);

它的接口和fread很像,参考fread。

fclose

关闭指定的文件流stream,使所有未写出的内容全部写出。

#include<stdio.h>

int fclose(FILE *stream);
  • stream:指定要关闭的文件流stream。
    返回值:如果成功返回0,失败返回EOF,同时会向全局变量errorn写入错误信息码。
fflush

fflush()函数的主要作用是把文件流的所有未写出的内容立刻写出。

#include<stdio.h>
 
int fflush(FILE *stream);

接口同fclose。

fseek

主要作用是在文件流里面为下一次读写指定位置。

#include<stdio.h>
 
int fseek(FILE *stream, long int offset, int whence);
  • stream:指定操作的文件流。

  • offset:指定位置。

  • whence:指定偏移方式,具体形式类似系统调用lseek。

返回值:成功返回0,失败返回-1,同时设置errno指出错误。

fgetc,getc,getchar

fgetc()从文件流取出下一个字节并把它作为一个字符返回,如果到达文件结尾或者出现错误的时候返回EOF。其中getchar()从标准输入读取数据。

#include<stdio.h>

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();
fputc,putc,putchar

fputc()函数把一个字符写道一个输出文件流中。如果成功返回写入的值,失败则返回EOF。其中puchar()函数将单个字符写道标准输出。

#include<stdio.h>
 
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar();
fgets和gets

fgets()函数从输入文件流stream读取一个字符串,并把读到的字符写到ptr指向的缓冲区,当遇到如下情况停止:遇到换行符,已经传输n-1个字符,或者到达文件尾。它会把遇到的换行符也传递到接收字符串里去,再加上一个表示结尾的空字节\0。
gets()函数从标准输入读取数据并丢弃遇到的换行符,它在接受字符串的尾部加上NULL。

#include<stdio.h>
 
char *fgets(char *ptr, int n, FILE *stream);
char *gets(char *ptr);
printf,fprintf,sprintf

printf系列函数能够对各种不同类型的参数进行格式化编排和输出。
printf函数把自己的输出送到标准输出。
fprintf函数把自己的输出送到一个指定的文件流。
sprintf函数把自己的输出和一个结尾空字符写到s指向的缓冲区。

#include<stdio.h>
 
int printf(const char *format, ...);
 
int sprintf(char *s, const char *format, ...);
 
int fprintf(FILE *stream, const char *format, ...);
  • format:指定输出的格式。

  • s:字符串缓冲区。

  • stream:指定的文件流。

scanf,fscanf和sscanf

scanf系列函数从一个文件流读取数据并写到指定地址的变量。
scanf函数将读入的值保存在对应的变量里。

#include<stdio.h>
 
int scanf(const char *format, ...);
 
int fscanf(FILE *stream, const char *format, ...);
 
int ssacnf(const char *s, const char *format, ...);
文件流错误

通过检查文件流的状态来确定是否发生错误。
ferror函数测试一个文件流的错误标识,如果被设置返回一个非0值,否则返回0。
feof函数测试一个文件流的文件尾标识,如果被设置就返回一个非0值,否则返回0。
clearerr函数清楚有stream指定的文件流的文件尾标识和错误标识。

#include<stdio.h>
 
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
文件流和文件描述符

每个文件流都有一个底层描述符相关联。
fileno函数可以确定文件流使用那个底层描述符。
fdopen函数在一个已经打开的文件描述符上创建一个新的文件流。

#include<stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);

使用标准I/O函数

标准IO函数的两个优点:

  • 标准I/O函数具有良好的移植性(Portability)。
  • 标准I/O函数可以利用缓冲提高性能。

标准IO函数的两个缺点:

  • 不容易进行双向通信。
  • 有时可能频繁调用fflush函数。
  • 需要以FILE结构体指针的形式返回文件描述符。

创建套接字时返回文件描述符,而为了使用标准I/O函数,只能将其转换为FILE结构体指针,先介绍器转换方法。

利用fdopen函数转换为FILE结构体指针

#include <stdio.h>

FILE * fdopen(int fildes.const char * mode);
//成功时返回转换的FILE结构体指针,失败时返回NULL。
  • fildes:需要转换的文件描述符。
  • mode:将要创建的FILE结构体指针的模式(mode)信息。

上述函数的第二个参数与fopen函数中的打开模式相同。常用的参数有读模式"r"和写模式"w"。

示例说明

#include <stdio.h>#include <fcnt1.h>
int main(void)
{
	FILE *fp;
	int fd = open("data.dat", O_WRONLY|O_CREAT[O_TRUNC);
	if(fd == -1)
	{
		fputs("file open error" , stdout);return -1;
	}
	fp = fdopen(fd,"w");
	fputs("Network C programming ln", fp);
	fclose(fp);
	return 0;
}

利用fileno函数转换为文件描述符

#inc1ude <stdio.h>
int fileno(FILE * stream);
//成功时返回转换后的文件描述符,失败时返回-1。

示例说明

#include <stdio.h>
#include <fcnt1.h>
int main(void){
	FILE *fp;
	int fd=open("data.dat",o_WRONLYlO_CREATIO_TRUNC);
	if(fd==-1)
	{
		fputs("file open error", stdout);
		return -1;
	}
	printf("First file descriptor: %d \n", fd);
	fp=fdopen(fd,"w");
	fputs("TCP/IP SOCKET PROGRAMMING \n", fp);
	printf("Second file descriptor: %d ln", fileno(fp));
	fclose(fp);
	return 0;
}

I/O分离问题

"流"分离带来的EOF问题

调用的fclose函数完全终止了套接字而不是半关闭。
解决办法:创建FILE指针前复制文件描述符即可,复制后另外创建1个文件描述符,然后利于各自的文件描述符生成读模式FILE指针和写模式FILE指针。这就为半关闭准备好了环境,因为套接字和文件描述符之间具有如下关系:
销毁所有文件描述符后才能销毁套接字。

但是这样做只是准备了半关闭环境,要进入真正的半关闭状态需要特殊处理,还剩余的一个文件描述符可以同时进行I/O。

复制文件描述符

在这里插入图片描述

#include <unistd.h>
int dup(int fildes);
int dup2(int fildes,int fildes2);
//成功时返回复制的文件描述符,失败时返回-1。
  • fildes:需要复制的文件描述符。
  • fildes2:明确指定的文件描述符整数值。
复制文件描述符后"流"的分离
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#tdefine BUF_SIZE 1024
int main(int argc,char *argv[]){

	int serv_sock,c1nt_sock;
	FILE* readfp;
	FILE* writefp;

	struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;
	char buf[BUF_SIZE]={0,};
	serv_sock=socket(PF_INET,sOCK_STREAM,0);
	memset(&serv_adr,0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=hton1(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
	listen(serv_sock, 5);
	clnt_adr_sz=sizeof(clnt_adr);
	clnt_sock=accept(serv_sock,(struct sockaddr*)&c1nt_adr,&c1nt_adr_sz);
	
	readfp=fdopen(clnt_sock,"r");
	writefp=fdopen(dup(clnt_sock),"w");
	
	fputs("FROM SERVER: Hi~ client? ln", writefp);
	fputs("I love all of the world ln", writefp);
	fputs( "You are awesome! ln", writefp);
	fflush(writefp);
	
	shutdown(fileno(writefp), SHUT_WR);
	fclose(writefp);
	
	fgets(buf, sizeof(buf),readfp);
	fputs(buf,stdout);
	fclose(readfp);
	return 0;
}

  • 调用fdopen函数生成FILE指针。特别是针对dup函数的返回值生成
    FILE指针。
  • 针对fileno函数返回的文件描述符调用shutdown函数。因此,服务器端进入半关闭状态,并向客户端发送EOF。这一行就是之前所说的发送EOF的方法。调用shutdown函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF。

优于select的epoll

select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接人上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。

基于select的I/O复用技术速度慢的原因

最主要的缺点是:

  • 调用select函数后常见的针对所有文件描述符的循环语句;
  • 每次调用select函数时都需要向该函数传递监视对象信息。

改进措施:
仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。
这种处理方式Linux支持方式是epoll,Windows的支持方式是IOCP。

select优点:

  • 服务器端接入者少;
  • 程序应具有兼容性。

实现epoll时必要的函数和结构体

能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。

  • 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句;
  • 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

epoll服务器端实现中需要的3个函数:

  • epoll_create:创建保存epoll文件描述符的空间;
  • epoll_ctl:向空间注册并注销文件描述符;
  • epoll_wait:与select函数类似,等待文件描述符发生变化。

select方式和epoll方式的不同

  • select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。

  • 为了添加和删除监视对象文件描述符,select方式中需要FD_SET、FD_CLR函数。但在epoll方式中,通过epoll_ctl函数请求操作系统完成。

  • select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。

  • select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。

结构体epoll_event
struct epol1_event{
	__uint32_tevents;
	epoll_data_t data;
}
typedef union epoll_data{
	void * ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}epoll_data_t;

声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。因此,无需像select函数那样针对所有文件描述符进行循环。

epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
//成功时返回epol1文件描述符,失败时返回-1。
  • size:epoll实例的大小。

调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程",但有些情况下名称不同,需要稍加注意。通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。换言之,size并非用来决定epoll例程的大小,而仅供操作系统参考。Linux 2.6.8之后的内核将完全忽略传入epoll_create函数的size参数,因为内核会根据情况调整epoll例程的大小。

epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。也就是说,该函数返回的文件描述符主要用与于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。

epoll_ctl

生成epoll_ctl例程后,应在其内部注册监视对象文件描述符,使用epoll_ctl函数。

#include <sys/epoll.h>

int epoll_ctl(int epfd,int op, int fd,struct epoll_event * event);
//成功时返回0,失败时返回-1。

  • epfd:用于注册监视对象的epoll例程的文件描述符。
  • op:用于指定监视对象的添加、删除或更改等操作。
  • fd:需要注册的监视对象文件描述符。
  • event:监视对象的事件类型。

按照如下形式调用epoll_ctl函数:

epol1_ct1(A,EPOLL_CTL_ADD,B,C);

第二个参数EPOLL_CTL_ADD意味着"添加",因此上述语句具有如下含义:
“epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。”

epoll_ct1(A,EPOLL_CTL_DEL,B,NULL);

上述语句中第二个参数EPOLL_CTL_DEL指"删除",因此该语句具有如下含义:
“从epoll例程A中删除文件描述符B。”

因为删除不需要监视类型,顾可以传递NULL,Linux2.9之前的内核不允许传递NULL(传递epoll_event结构体地址就好)。

op传递常量及含义
  • EPOLL_CTL_ADD:将文件描述符注册到epoll例程。
  • EPOLL_CTL_DEL:从epoll例程中删除文件描述符。
  • EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
event传递常量及含义
  • EPOLLIN:需要读取数据的情况。
  • EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
  • EPOLLPRI:收到OOB数据的情况。
  • EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
  • EPOLLERR:发生错误的情况。
  • EPOLLET:以边缘触发的方式得到事件通知。
  • EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向
    epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

注意:可以通过位或运算同时传递多个上述参数。

epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents,int timeout);
//成功时返回发生事件的文件描述符数,失败时返回-1。
  • epfd:表示事件发生监视范围的epoll例程的文件描述符。
  • events:保存发生事件的文件描述符集合的结构体地址值。
  • maxevents:第二个参数中可以保存的最大事件数。
  • timeout:以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
    注意:第二个参数所指缓冲需要动态分配。
int event_cnt;
struct epoll_event * ep_events;
....
ep_events = malloc(sizeof(struct epoll_event)* EPOLL_SIZE);	//EPOLL_SIZE是宏常量
....
event_cnt = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

条件触发和边缘触发

条件触发中,只要输入缓冲有数据就会一直通知该事件(epoll默认以条件触发方式工作)。
边缘出发中输入缓冲收到数据时仅注册1次该事件,即使输入缓冲中还留有数据,也不会再进行注册。

边缘触发的服务端实现中必知的两点

  1. 通过error变量验证错误原因(验证输入缓冲是否为空);
  2. 为了完成非阻塞(Non-blocking)I/O,更改套接字特性(以阻塞方式工作的read & write函数有可能引起服务器端的长时间停顿)。
error变量验证错误

Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息.
Linux声明了如下全局变量:

int errno;

为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到errno变量中的值都不同,没必要记住所有可能的值。学习每种函数
的过程中逐一掌握,并能在必要时参考即可。本节只介绍如下类型的错误:
“read函数发现输入缓冲中没有数据可读时返回-1,同时在errmo中保存EAGAIN常量。”

更改套接字特性

Linux提供更改或读取文件属性的如下方法:

include <fcnt1.h>
int fcntl(int filedes, int cmd,. .);
//成功时返回 cmd参数相关值,失败时返回-1。
  • filedes:属性更改目标的文件描述符。
  • cmd:表示函数调用的目的。
    从上述声明中可以看到,fentl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性( int型)。反之,如果传递F_SETFL
    可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下2条语句。
int flag = fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flaglo_NONBLOCK);

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read & write函数时,无论是否存在数据,都会形成非阻塞文件(套接字)。

条件触发和边缘触发孰优孰劣

边缘触发的优点:可以分离接受数据和处理数据的时间点!
从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为"只要使用边缘触发就一定能提高速度。"

多线程服务器端

线程概念

进程:在操作系统构成单独执行流的单位。
线程:在进程构成单独执行流的单位。

进程间独立的内存:
在这里插入图片描述

线程的内部结构:
在这里插入图片描述

线程的创建和运行

#include <pthread.h>
int pthread_create(
pthread_t * restrict thread, const pthread_attr_t * restrict attr,void *(*start_routine)(void *),void * restrict arg
);
//成功时返回0,失败时返回其他值
  • thread:保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID。
  • attr:用于传递线程属性的参数,传递NULL时,创建默认属性的线程。
  • start_routine:相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)。
  • arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值。

线程示例

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

void * thread_main(void *arg);

int main(int argc,char *argv[])
{
	pthread_t t_id;
	int thread_param = 5;
	
	if(pthread_create(&t_id,NULL,thread_main,(void*)&thread_param)!=0)
	{
		puts("pthread_create() error");
		return -1;
	};
	sleep(10);
	puts("end of main");
	return 0;
}

void* thread_main(void *arg)
{
	int i;
	int cnt = *((int*)arg);
	for(i = 0; i < cnt;i++)
	{
		sleep(1);
		puts("running thread");
	}
	return NULL;
}

输出:
在这里插入图片描述

等待线程结果

#include <pthread.h>
itn pthread_join(pthread_t thread, void * status);
//成功时返回0,失败时返回其他值。
  • thread:该参数ID的线程终止后才会从该参数返回。
  • status:保存线程的main函数返回值的指针变量地址值。

注意:调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。
调用pthread_join函数:
在这里插入图片描述

可在临界区内调用的函数

多个线程同时执行一部分代码,可能会引起问题。因此根据临界区是否引起问题,函数可分为以下两类:

  • 线程安全函数;
  • 非线程安全函数

线程安全函数被多个线程同时调用时也不会引发问题。反之,非线程安全函数被同时调用时会引发问题。但这并非关于有无临界区的讨论,线程安全的函数中同样可能存在临界区。只是在线程安全函数中,同时被多个线程调用时可通过一些措施避免问题。
幸运的是,大多数标准函数都是线程安全的函数。更幸运的是,我们不用自己区分线程安全的函数和非线程安全的函数(在Windows程序中同样如此)。因为这些平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。比如,gethostbyname函数就不是线程安全的函数:

struct hostent * gethostbyname( const char * hostname);

同时提供线程安全的同一功能的函数。

struct hostent * gethostbyname_r(
const char * name,struct hostent * result,char * buffer, intbuflen,
int * h_errnop);

线程安全函数的名称后缀通常为r(这与Windows平台不同)。既然如此,多个线程同时访问的代码块中应该调用gethostbyname_r,而不是gethostbyname。可以通过如下方法自动将gethostbyname函数调用改为gethostbyname_r函数调用!
“声明头文件前定义_REENTRANT宏。”
另外,无需为了上述宏定义特意添加#define语句,可以在编译时通过添加-D_REENTRANT选项定义宏。

root@my_linux: /tcpip# gcc -D_REENTRANT mythread.c -o mthread -lpthread

线程存在的问题和临界区

多个线程访问同一变量是问题

由于值的改变需要CPU运算完成,CPU从内存中获取数据,计算后在把结果写回内存中的变量中;这其中多个线程操作同一变量时会发生问题。
在这里插入图片描述

临界区

函数内同时运行多个线程时引起问题的多条语句构成的代码块。

void * thread_inc(void * arg){
	int i;
	for(i = 0; i < 50088080; i++)
	num += 1;//临界区
	return NULL;
}

void * thread_des(void * arg){
	int i;
	for(i = 0; i< 50080000; i++)
	num -= 1;//临界区
	return NULL;
}

线程同步

同步的两面性

线程同步需要考虑的两个方面:

  • 同时访问同一内存时发生的情况。
  • 需要指定访问同一内存空间的线程执行顺序的情况。
互斥量

作用:主要用来解决线程同步访问的问题。

创建和销毁
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t *  attr);
int pthread_mutex_destory(pthread_mutex_t * mutex);
//成功时返回0,失败时返回其他值。
  • mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
  • attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL。

从上述函数声明中也可看出,为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量:

pthread_mutex_t mutex;

该变量的地址将传递给pthread_mutex_init函数,用来保存操作系统创建的互斥量(锁系统)。调用pthread_mutex_destroy函数时同样需要该信息。如果不需要配置特殊的互斥量属性,则向第二个参数传递NULL时,可以利用PTHREAD_MUTEX_INITIALIZER宏进行如下声明:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

注意:推荐尽可能使用pthread_mutex_init函数进行初始化,因为通过宏定义进行初始化时很难发现发生的错误。

锁住和释放
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t*mutex);int pthread_mutex_unlock(pthread_mutex_t* mutex);
//成功时返回0,失败时返回其他值。

其调用位置如下:

pthread_mutex_lock(&mutex);
//临界区的开始
. . . . .
//临界区的结束
pthread_mutex_unlock(&mutex);

线程退出临界区是,如果忘了调用pthread_mutex_unlock函数,那么其他为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况被称为"死锁"。

优化方面:最大限度减少互斥量lock、unlock函数的调用次数。

互斥量示例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM_THREAD 100

void * thread_inc(void * arg);
void * thread_des(void * arg);
long long num = 0;	//long long int
pthread_mutex_t mutex;

int main(int argc,char * argv[])
{

	pthread_t thread_id[NUM_THREAD];
	int i;
	
	pthread_mutex_init(&mutex,NULL);
	
	printf("sizeof long long:%ld\n",sizeof(long long));
	for(i = 0;i < NUM_THREAD;i++)
	{
		if(i%2)
			pthread_create(&(thread_id[i]),NULL,thread_inc,NULL);
		else
			pthread_create(&(thread_id[i]),NULL,thread_des,NULL);
	}
	
	for(i = 0;i < NUM_THREAD;i++)
		pthread_join(thread_id[i],NULL);
	
	printf("result:%lld\n",num);
	pthread_mutex_destroy(&mutex);
	return 0;
}

void * thread_inc(void * arg)
{
	int i;
	pthread_mutex_lock(&mutex);
	for(i = 0;i < 50000000;i++)
		num += 1;
	pthread_mutex_unlock(&mutex);
	return NULL;
}

void * thread_des(void * arg)
{
	int i;
	pthread_mutex_lock(&mutex);
	for(i = 0;i < 50000000;i++)
		num -= 1;
	pthread_mutex_unlock(&mutex);
	return NULL;
}

输出:
在这里插入图片描述

信号量

作用:可以用来解决控制线程顺序的问题,其本身可用于负责数据操作过程中的互斥、同步等功能。使用"二进制信号量"完成"控制线程顺序"为中心的同步方法。

创建和销毁
#include <semaphore.h>
int sem_init(sem_t * sem, int pshared,unsigned int value);int sem_destroy(sem_t * sem);
//成功时返回0,失败时返回其他值。
  • sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
  • pshared:传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。我们需要完成同一进程内的线程同步,故传递0。
  • value:指定新创建的信号量初始值。
锁住和释放
#include <semaphore.h>
int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
//成功时返回0,失败时返回其他值。
  • sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1。

调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着"信号量值"整数。该值在调用sem_post函数时增1,调用sem_wait函数时减1。但信号量的值不能小于0,因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为O并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1)。

sem_wait( &sem);//信号量变为0...
//临界区的开始
// . . . . .
//临界区的结束
sem_post(&sem);//信号量变为1...
信号量示例

应用场景:
线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加,该过程共进行5次,完成后输出总和并退出程序。

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

void * read(void * arg);
void * accu(void * arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;

int main(int argc,char * argv[])
{
	pthread_t id_t1,id_t2;
	sem_init(&sem_one,0,0);
	sem_init(&sem_two,0,1);
	
	pthread_create(&id_t1,NULL,read,NULL);
	pthread_create(&id_t2,NULL,accu,NULL);
	
	pthread_join(id_t1,NULL);
	pthread_join(id_t2,NULL);
	
	sem_destroy(&sem_one);
	sem_destroy(&sem_two);
	
	return 0;
}

void * read(void * arg)
{
	int i;
	for(i = 0; i < 5; i++)
	{
		fputs("Input num:",stdout);
		sem_wait(&sem_two);
		scanf("%d",&num);
		sem_post(&sem_one);
	}
	return NULL;
}

void * accu(void * arg)
{
	int sum = 0,i;
	for(i = 0; i < 5; i++)
	{

		sem_wait(&sem_one);
		sum += num;
		sem_post(&sem_two);
	}
	printf("Result:%d\n",sum);
	return NULL;
}

输出:
在这里插入图片描述

线程的销毁

Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下2种方法之一加以明确。否则由线程创建的内存空间将一直存在。

  • 调用pthread _join函数。
  • 调用pthread_detach函数。

之前调用过pthread_join函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进人阻塞状态。因此,通常通过如下函数调用引导线程销毁。

#include <pthread.h>
int pthread_detach(pthread_t thread);
//成功时返回0,失败时返回其他值。
  • thread:终止的同时需要销毁的线程ID。

调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能再针对相应线程调用pthread_join函数,这需要格外注意。虽然还有方法在创建线程时可以指定销毁时机,但与pthread_detach方式相比,结果上没有太大差异,故省略其说明。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值