本文讨论服务器端多线程并发的操作和限制:
基于实验结果和百度结果:
实验基础:服务器和客户端,服务器为每个客户端连接开辟线程,验证服务器多线程的最大支持数目
实验环境:ubuntu 12.04
实验结果: 1、一切系统默认设置的情况下,最多接收了381个链接,也即开启了381个线程。
实验总结:为得到尽可能多的线程,可从下面几个方面考虑:
一、及时回收线程资源
默认情况下,线程的资源是在主线程结束时才会被回收,在线程结束时资源并不能马上被释放;
我们可以调用pthread_detach()函数,在主线程或是子线程将子线程与主线程分离,这样一旦子线程结束就会立即释放子线程占用的资源,这些资源可供新的连接使用
二、栈大小对线程数目的限制
ubuntu中默认的栈空间是8MB,而每个进程的虚拟内存空间是3GB,那么正常情况下可以开启382个线程,这与上述实验结果吻合
我们可以更改栈大小,进而创建更多的线程。
更改栈大小的两个方法:1、ulimit -s 1024(改为1M) 2、使用pthread_attr_t线程属性
三、系统设置的最大线程数目
linux下对线程的数目有两个地方的设置:
1、linux系统中所有进程所能开启的总的线程数:
cat /proc/sys/kernel/threads-max
15783
2、linux 系统中单个进程的最大线程数有其最大的限制 PTHREAD_THREADS_MAX
这个限制可以在 /usr/include/bits/local_lim.h 中查看;ubuntu中位于/usr/include/i386-linux-gnu/bits/local_lim.h
/* The number of threads per process. */
#define _POSIX_THREAD_THREADS_MAX 64
/* We have no predefined limit on the number of threads. */
#undef PTHREAD_THREADS_MAX
/* Minimum size for a thread. We are free to choose a reasonable value. */
#define PTHREAD_STACK_MIN 16384
可以看出,posix标准中是64个线程,但在ubuntu中并没有预先定义PTHREAD_THREADS_MAX
对于 linux这个值一般是 1024,对于 nptl 则没有硬性的限制,仅仅受限于系统的资源,这个系统的资源主要就是线程的 stack 所占用的内存,用 ulimit -s 可以查看默认的线程栈大小,ubuntu下这个值是8M
四、文件描述的限制
在socket连接中,每个链接需要一个读写描述符(实质就是文件描述符);系统默认下,每个进程所能使用的最大描述符数目是1024;
这也是为什么将栈大小改为1M后,最大线程数据不能超过1020的原因所在。
在根用户下将文件描述符修改为5000:ulimit -n 5000;然后切换到普通用户,重新运行server,此时最多可以达到3000+,这也是符合理论值的,3G的虚拟进程内存,1M的栈空间,理论值可以达到3050个线程。
具体的修改方法:
五、端口数目限制
服务器连接中需要为每个连接分配端口号,linux中端口的分配有一个范围,超出该范围将不能建立连接,该范围大概有近三万的端口可供使用
贴上服务器端代码
void *thread_handle(void *fd);
void sigint_handle(int sig);
static int server_fd = -1;
int get_thread_stacksize_for_attr(pthread_attr_t * p_pattr)
{
int status = 0;
size_t size = 0;
//printf("default size:%d\n", size);
status = pthread_attr_getstacksize(p_pattr, &size);
if(0 != status)
{
printf("pthread_attr_getstacksize err [%d]\n",status);
return -1;
}
printf("current thread stack size:%d\n", size);
return 0;
}
int set_thread_stacksize(pthread_attr_t * p_pthread_attr_t,size_t size)
{
//pthread_attr_t pattr;
int status = 0;
//printf("default size:%d\n", size);
status = pthread_attr_setstacksize(p_pthread_attr_t, size);
if(0 != status)
{
printf("pthread_attr_getstacksize err [%d]\n",status);
return -1;
}
printf("set thread stack size:%d\n", size);
return 0;
}
int main(int argc,char *argv[])
{
int *client_fd;
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
socklen_t client_address_len = 0;
int ret = -1;
char client_address_buf[20] = {0};
pthread_t thread_id = -1;
pthread_attr_t pattr;
pthread_attr_init(&pattr);
set_thread_stacksize(&pattr,1024*1024);
get_thread_stacksize_for_attr(&pattr);
if(SIG_ERR == signal(SIGINT,sigint_handle))
{
perror("signal");
exit(-1);
}
server_fd = socket(AF_INET,SOCK_STREAM,0);
if(server_fd < 0)
{
perror("socket");
exit(-1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IPADDRESS);
ret = bind(server_fd,(const struct sockaddr *)&server_addr,sizeof(server_addr));
if(ret < 0)
{
perror("bind");
exit(-1);
}
ret = listen(server_fd,BACKLOG);
if(ret < 0)
{
perror("listen");
exit(-1);
}
printf("listening\n");
while(1)
{
client_fd = (int *)malloc(sizeof(int));
*client_fd = accept(server_fd,(struct sockaddr *)&client_addr,(socklen_t *)&client_address_len);
if(client_fd < 0)
{
perror("accept");
exit(-1);
}
inet_ntop(AF_INET,(const void *)&client_addr.sin_addr,client_address_buf,sizeof(client_address_buf));
printf("client IP:%s port:%hu client_fd:%d\n",client_address_buf,ntohs(client_addr.sin_port),*client_fd);
ret = pthread_create(&thread_id,&pattr,thread_handle,(void *)client_fd);
if(ret)
{
perror("pthread_create");
exit(-1);
}
//pthread_detach(thread_id);
//get_thread_stacksize_for_attr(&pattr);
}
return 0;
}
void *thread_handle(void *fd)
{
char recv_buf[30] = {0};
int ret = -1;
int *client_fd = (int *)fd;
//pthread_detach(pthread_self());
while(1)
{
ret = recv(*client_fd,(void *)recv_buf,sizeof(recv_buf),0);
if(-1 == ret)
{
perror("recv");
exit(-1);
}
else if(0 == ret)
{
printf("client disconnect,client_fd:%d\n",*client_fd);
if(close(*client_fd))
{
perror("close");
exit(-1);
}
free(client_fd);
pthread_exit(0);
}
printf("client_fd:%d bytes:%d %s\n",*client_fd,ret,recv_buf);
memset(recv_buf,0,sizeof(recv_buf));
}
}
void sigint_handle(int sig)
{
if(SIGINT != sig)return;
printf("\nserver termated\n");
close(server_fd);
exit(1);
}
实验目的:服务器进程所支持的最大线程(考虑4点:栈大小、是否及时回收线程资源、系统默认设置的线程数、系统默认设置的文件描述符数量)
实验过程:client.c连接服务器成功后,每隔一秒发送一次数据;server.c为每一个连接创建线程,接收数据并打印。
试验一:更改栈空间
1、server处于监听状态是2248K,一旦接收一个链接变为10444K,意味着一个线程大概8MB;client运行过程则是2004K
2、将线程堆栈的默认值改为1M
server处于监听状态是2252K,一旦接收一个连接后变为3280K,增加了1028K,意味着一个线程大概为1M;增加第二个线程增加仍为1028K;client仍然为2004
3、当打开二十个连接,也即在server端打开二十个线程,内存增加为20*1028
top - 18:10:44 up 8:55, 4 users, load average: 0.04, 0.04, 0.05
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 1.7%us, 0.7%sy, 0.0%ni, 97.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1024764k total, 628068k used, 396696k free, 136756k buffers
top - 18:12:05 up 8:56, 4 users, load average: 0.01, 0.03, 0.05
Tasks: 22 total, 0 running, 22 sleeping, 0 stopped, 0 zombie
Cpu(s): 2.0%us, 1.0%sy, 0.0%ni, 97.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1024764k total, 630024k used, 394740k free, 136756k buffers
Swap: 1046524k total, 0k used, 1046524k free, 330296k cached
这种情况下,每个线程只有97K的内存占用(猜测,这可能是实际占用的存储空间,真正用到的空间)
free 628060 629876 每个进程只用了90K
实验二:不回收资源,不改变栈空间的情况下,server支持的最大线程数目
1、超过三百个链接(大概在318之后) 会挂掉;第二次试验在360挂掉;第三次380(第四次,第五次)
2、即使关闭一部分连接,但其资源没有被立即释放
3、用测试程序测试,可以打开的最大线程数是382
ps -aux | grep "server" (VSZ:该进程所占用的虚拟内存量(KB);RSS:该进程所占用的固定的内存量(KB);)
rss 实际开销的物理内存,这些内存都是该进程现在正在使用的物理内存。
vsz 虚拟内存大小,也就是说,程序目前没有使用,但是可能会分配的内存大小。
vsz:
运行200个链接(200个线程),内存占用1641452((1641452-2252)/200 =8196=8.0039MK)
运行300个链接(300个线程),内存占用2461052((2461052-2252)/300 =8196=8.0039MK)
实际的内存空间根本没有这么大(虚拟机实际上只有1G的空间),猜测1,虚拟内存;猜测2,每个线程的实际占用内存只有100K左右。
rss:
user 3977 0.0 0.0 2252 316 pts/0 S+ 10:44 0:00 ./server (无连接)
user 3977 1.2 0.1 2461052 1632 pts/0 Sl+ 10:44 0:00 ./server (300连接)
(1632-316)/300=4.38K
使用top -H -p命令
top - 10:27:02 up 42 min, 3 users, load average: 0.11, 0.12, 0.11
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 2.7%us, 0.7%sy, 0.0%ni, 96.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1024764k total, 461624k used, 563140k free, 129184k buffers
Swap: 1046524k total, 0k used, 1046524k free, 189616k cached
top - 10:30:35 up 46 min, 3 users, load average: 0.18, 0.13, 0.12
Tasks: 301 total, 0 running, 301 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.7%us, 4.7%sy, 0.0%ni, 94.6%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1024764k total, 468584k used, 556180k free, 129184k buffers
Swap: 1046524k total, 0k used, 1046524k free, 189616k cached
(468584-461624)/300 = 23.2K (使用的物理内存的总量)
这个值和理论完全相符,因为 32 位 linux 下的进程用户空间是 3G 的大小,也就是 3072M,用 3072M 除以 8M 得 384,但是实际上代码段和数据段等还要占用一些空间,这个值应该向下取整到 383,再减去主线程,得到 382。
那为什么 linuxthreads 上还要少一个线程呢?这可太对了,因为 linuxthreads 还需要一个管理线程
为了突破内存的限制,可以有两种方法
1) 用 ulimit -s 1024 减小默认的栈大小
2) 调用 pthread_create 的时候用 pthread_attr_getstacksize 设置一个较小的栈大小
要注意的是,即使这样的也无法突破 1024 个线程的硬限制,除非重新编译 C 库
实验三:回收资源(pthread_detach),当然了,同时运行的连接还是不能达到400;关闭的连接资源被马上释放,可以供新的连接所使用(当运行三百连接时,先关闭一百个连接,再打开一百个连接,是可以的)
结论:pthread_detach可以在线程结束后,马上释放资源!
实验三:不回收资源,但减少线程栈空间:将线程属性中的栈空间改为1MK
user 4643 0.0 0.0 2252 312 pts/0 S+ 10:55 0:00 ./server
user 4643 1.2 0.1 310652 1628 pts/0 Sl+ 10:55 0:01 ./server
可以看出,vsz大大减少 (310652-2252)/300 = 1028K 而rss则几乎没有发生变化!
打开1000个链接(1000个线程)
user 5283 0.9 0.4 1030384 4532 pts/0 Sl+ 11:01 0:00 ./server
(4532-316)/1000=4.22K
(1030384-2252)/1000=1028K
此时的最大连接数1018(小于1020个),但测试程序跑起来的时候是3054个线程,这里的原因在于文件描述符,ubuntu中每个进程的文件描述符最大为1024个,ubuntu中并没有对进程的最大线程数目做出限制。
[root@localhost test]# ulimit -s 1024
[root@localhost test]# ./test
pthread_create() failure
Max pthread num is 3054
1、查看最大线程数(系统所有进程所能开启的总的线程数):
cat /proc/sys/kernel/threads-max
15783
2、Ulimit命令
设置限制 可以把命令加到profile文件里,也可以在/etc/security/limits.conf文件中定义
限制。
命令参数
-a 显示所有限制
-c core文件大小的上限
-d 进程数据段大小的上限
-f shell所能创建的文件大小的上限
-m 驻留内存大小的上限
-s 堆栈大小的上限
-t 每秒可占用的CPU时间上限
-p 管道大小
-n 打开文件数的上限
-u 进程数的上限
-v 虚拟内存的上限
除可用Ulimit命令设置外,也可以在/etc/security/limits.conf文件中定义限制。
domino type item value
domino是以符号@开头的用户名或组名,*表示所有用户,type设置为hard or soft。item指
定想限制的资源。如cpu,core nproc or maxlogins。value是相应的限制值。
系统限制默认值(ubuntu)
user@instant-contiki:~/socket$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7891
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7891
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
user@instant-contiki:~/socket$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 12.04.2 LTS
Release: 12.04
Codename: precise
3、linux 系统中单个进程的最大线程数有其最大的限制 PTHREAD_THREADS_MAX
这个限制可以在 /usr/include/bits/local_lim.h 中查看
ubuntu中位于/usr/include/i386-linux-gnu/bits/local_lim.h
/* The number of threads per process. */
#define _POSIX_THREAD_THREADS_MAX 64
/* We have no predefined limit on the number of threads. */
#undef PTHREAD_THREADS_MAX
/* Minimum size for a thread. We are free to choose a reasonable value. */
#define PTHREAD_STACK_MIN 16384
对 linuxthreads 这个值一般是 1024,对于 nptl 则没有硬性的限制,仅仅受限于系统的资源
可以看出,posix标准中是64个线程,但在ubuntu中并没有预先定义PTHREAD_THREADS_MAX,这应该会受限于系统资源
这个系统的资源主要就是线程的 stack 所占用的内存,用 ulimit -s 可以查看默认的线程栈大小,ubuntu下这个值是8M
一、2.4内核与2.6内核的主要区别
在2.4内核的典型系统上(AS3/RH9),线程是用轻量进程实现的,每个线程要占用一个进程ID,在服务器程序上,如果遇到高点击率访问,会造成进程表溢出,系统为了维护溢出的进程表,会有间歇的暂停服务现象,而2.6内核就不会发生由于大量线程的创建和销毁导致进程表溢出的问题
二、线程结束必须释放线程堆栈
就是说,线程函数必须调用pthread_exit()结束,否则直到主进程函数退出才释放,特别是2.6内核环境,线程创建速度飞快,一不小心立刻内存被吃光,这一点反倒是2.4内核环境好,因为2.4内核创建的是进程,而且线程创建速度比2.6内核慢几个数量级。特别提醒,在64位CPU,2.6内核创建线程的速度更加疯狂,要是太快的话,加上usleep ()暂停一点点时间比较好
三、不要编需要锁的线程应用
只有那些不需要互斥量的程序才能最大限度的利用线程编程带来的好处,否则只会更慢,2.6内核是抢占式内核,线程间共享冲突发生的几率远比2.4内核环境高,尤其要注意线程安全,否则就算是单CPU也会发生莫名其妙的内存不同步(CPU的高速缓存和主存内容不一致),Intel的新CPU为了性能使用NUMA架构,在线程编程中一定要注意扬长避短。
四、单进程服务器最大并发线程数与内存
很有趣,在默认的ulimit参数下,不修改内核头文件
AS3 512M内存最多1000并发持续连接
CentOS4.3 512M内存最多300并发持续连接
似乎是CentOS不如AS3,这里主要原因是ulimit的配置造成,两个系统默认的配置差距很大,要想单进程维持更多线程接收并发连接,就要尽量缩小 ulimit -s的参数,插更多的内存条,单进程服务器上2000并发一点都不难,POSIX默认的限制是每进程64线程,但NTPL并非纯正POSIX,不必理会这个限制,2.6内核下真正的限制是内存条的插槽数目(也许还有买内存的钱数)
最近几天的编程中,注意到在32位x86平台上2.6内核单进程创建最大线程数=VIRT上限/stack,与总内存数关系不大,32位x86系统默认的VIRT上限是3G(进程运行的虚拟地址空间,32位OS下是0-1G的OS和1-4的进程空间),默认 stack大小是10240K,因此单进程创建线程默认上限也就300(3072M / 10240K),用ulimit -s 修改stack到1024K则使上限升到大约3050。我手头没有64位系统,不知道2.6内核在64位上单进程创建线程上限(实际上是本人懒得在同事的机器上装fc4_x86_64)。
前些天买了一套廉价的64位x86系统(64位赛杨+杂牌915主板),安装了CentOS4.3的x86_64版本,跑了一遍下面的小程序,得到的结果是:在ulimit -s 4096的情况下,单进程最大线程数在16000多一点,用top看
VIRT 的上限是64G,也就是36位, cat /proc/cpuinfo的结果是:address sizes : 36 bits physical, 48 bits virtual, 和我想象的标准64位系统不同, 我一直以为64位系统的内存空间也是64位的
附注1:
单位里某BSD FANS用AMD64笔记本跑小程序测试线程创建速度(线程创建后立即phread_detach()然后紧跟着pthread_exit(),共计 100万个线程),同样源码OpenBSD竟然比FreeBSD快了3倍,什么时候OpenBSD也变得疯狂起来了?
附注2:
测试单进程创建线程上限C源码(test.c):
#include
#include
#include
#include
#include
void * thread_null(void);
int main(int argc, char *argv[])
{
unsigned int i;
int rc;
pthread_t pool_id[65536]; //线程ID
sleep(1);
//创建线程
for(i = 0; i < 65536; i++) {
rc = pthread_create(pool_id + i, 0, (void *)thread_null, NULL);
if (0 != rc) {
fprintf(stderr, "pthread_create() failure\r\nMax pthread num is %d\r\n", i);
exit(-1);
}
}
fprintf(stdout, "Max pthread num is 65536\r\nYour system is power_full\r\n");
exit(0);
}
void * thread_null(void)
{
pthread_detach(pthread_self());
sleep(60);
pthread_exit(NULL);
}
编译: