上节提到了accpet()函数处理客户端连接时,当有客户端连接时,函数会返回。那么问题来了,这样作为一个服务器就只能给一个客户端服务了,显然这样的服务器就只能拿来自己玩了。那么怎么处理这个多并发问题呢?
首先可以先用多进程实现,在accept返回后创建一个子进程负责和客户端通信,父进程继续执行accpet()。
进程:正在运行的程序及其占用的资源(CPU、内存、系统资源等)叫做进程(操作系统原理)。是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。
进程ID:每一个运行的进程都有自己独一无二的PID(Process ID)——进程ID。Linux 中维护着一个数据结构叫做进程表,保存当前加载在内存中的所有进程的有关信息,其中包括进程的、进程的状态、 命令字符串等,操作系统通过进程的 PID 对它们进行管理,这些 PID 是进程表的索引。
init进程:Linux内核在启动的后阶段会创建init进程来执行程序/sbin/init,该进程是系统运行的第一个进程,进程号为 1,称为 Linux 系统的初始化进程,该进程会创建其他子进程来启动不同写系统服务,而每个服务又可能创建不同的子进程来执行不同的程序。所以init进程是所有其他进程的“祖先”。
"fork"见名思义就是分叉的意思,主进程通过fork()系统调用我们可以创建一个和当前进程印象一样的新进程.我们通常将新进程称为子进程,而当前进程称为父进程。内核将父进程的用户地址空间的内容复制给子进程,这样父子进程拥有各自独立的用户空间,当父进程修该变量的值时不会影响子进程中的相应变量。
子进程具体会继承父进程哪些东西,又不会继承哪些东西呢?(面试考点!!!)
用户号UIDs和用户组号GIDs
环境Environment
堆栈
共享内存
打开文件的描述符(所以实现进程之间通信时可以采用管道通信方式)
执行时关闭(Close-on-exec)标志
信号(Signal)控制设定
进程组号
当前工作目录
根目录
文件方式创建屏蔽字
资源限制
控制终端
子进程独有
进程号PID
不同的父进程号
自己的文件描述符和目录流的拷贝
子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
不继承异步输入和输出
父进程和子进程拥有独立的地址空间和PID参数。
链接1:https://www.nowcoder.com/questionTerminal/d86fd6f986e24bc2bb67e8b7a919eea7
链接2:https://blog.csdn.net/ygm_linux/article/details/50683877
在我们编 程的过程中,一个函数调用只有一次返回(return),但由于fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()后,需要通 过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进程在运行,而如果返回值<0的话,说明fork()系统调用出错。fork 函数调用失败的原因主要有两个:
fork使用过程中应该注意:
我们创建了一个子进程都是让子进程继续执行父进程的文本段,但更多的情况下是让该进程去执行另外一个 程序。这时我们会在fork()之后紧接着调用exec*()系列的函数来让子进程去执行另外一个程序。其中exec*()是一些列的函数,其 原型为:
pid_t fork(void);
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
在fork()之后常会紧跟着调用exec来执行另外一个程序,而exec会抛弃父进程的文本段、数据 段和堆栈等并加载另外一个程序,所以现在的很多fork()实现并不执行一个父进程数据段、堆和栈的完全副本拷贝。作为替代, 使用了写时复制(CopyOnWrite)技术: 这些数据区域由父子进程共享,内核将他们的访问权限改成只读,如果父进程和子进程 中的任何一个试图修改这些区域的时候,内核再为修改区域的那块内存制作一个副本。
vfork()是另外一个可以用来创建进程的函数,他与fork()的用法相同,也用于创建一个新进程。 但vfork()并不将父进程的地址 空间完全复制到子进程中,因为子进程会立即调用exec或exit(),于是也就不会引用该地址空间了。不过子进程再调用exec()或 exit()之前,他将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响 了父进程空间的数据可能会导致父进程的执行异常。此外,vfork()会保证子进程先运行,在他调用了exec或exit()之后父进程才 可能被调度运行。因为父进程阻塞,如果子进程依赖于父进程的进一步动作,则会导致死锁。
pid_t vfork(void);
当一个进程正常或异常退出时,内核就会向其父进程发送SIGCHLD信号。因为子进程退出是一个异步事件,所以这种信号也是内核向父进程发送的一个异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即将被执行的函数,父进程可以 调用wait()或waitpid()可以用来查看子进程退出的状态。
如果一个已经终止、但其父进程尚未对其调用wait进行善后处理 (获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵死进程(zombie),ps命令将僵死 进程的状态打印为Z。如果子进程已经终止,并且是一个僵死进程,则wait立即返回该子进程的状态。所以,我们在编写多进程 程序时,好调用wait()或waitpid()来解决僵尸进程的问题。如果如果父进程在子进程退出之前退出了,这时候子进程就变成了孤儿进程。当然每一个进程都应该有一个独一无二的父进 程,init进程就是这样的一个“慈父”,Linux内核中所有的子进程在变成孤儿进程之后都会被init进程“领养”,这也意味着孤 儿进程的父进程终会变成init进程。
pid_t wait(int *status); //暂停执行,直到一个子进程结束,成功返回进程pid,否则返回-1
pid_t waitpid(pid_t pid, int *status, int options); //等待指定子进程结束,options指定等待方式,可以非阻塞
wait()与waitpid()详解
多进程改写服务器(部份代码)
while(1)
{
printf("Start accept new client incoming...\n");
clifd=accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if(clifd < 0)
{
printf("Accept new client failure: %s\n", strerror(errno));
continue;
}
printf("Accept new client[%s:%d] successfully\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
pid = fork();
if( pid < 0 )
{
printf("fork() create child process failure: %s\n", strerror(errno));
close(clifd);
continue;
}
else if( pid > 0 )
{
/* Parent process close client fd and goes to accept new socket client again */
close(clifd);
continue;
}
else if ( 0 == pid )
{
//do some thing here!
}
}
return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;main函数中调用程序退出
exit函数是退出应用程序,只要调用程序退出。
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间 需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间 和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据 交换)变得非常高效。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程是独立调度和分派的基本单位。
在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境 (register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。
函数原型
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。
第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:
typedef struct {
int detachstate; 线程的分离状态
int schedpolicy; 线程调度策略
struct sched_param schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; 线程栈的位置
size_t stacksize; 线程栈的大小
}pthread_attr_t;
第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务的入口(函数);
第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
每个线程都能够通过pthread_self()来获取 自己的线程ID(pthread_t类型)。
创建过程
无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁 后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资 源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销 毁,取决于线程属性。
创建的线程有不同的属性,pthread_create创建后每个线程默认是可会合joinable 状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。主线程调用 pthread_join 这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。如果执行成功,将返回0,如果失败则返回一个错误号。所以我们创建线程时一般会将线程设置为detach分离状态。
int pthread_join(pthread_t thread, void **retval);
!!!创建的多个线程如果是可会合joinable状态,一定要按顺序配套调用pthread_join ()。否则运行结果不确定。
正确如下:
pthread_create(&tid1, NULL, func1, NULL);
pthread_join(tid1, NULL);
pthread_create(&tid2, NULL, func2, NULL);
pthread_join(tid2, NULL);
pthread_create(&tid3, NULL, func3, NULL);
pthread_join(tid3, NULL);
因为所有的线程都是在同一进程空间运行,所以在一个线程中如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于该资源访问修改相关的代码就叫做临界区。
怎么让一个线程访问临界区时,内容不会被其他的线程改变。就引入了互斥锁。
互斥锁就是在一个线程访问临界区时加锁,则其他线程阻塞或者非阻塞,访问后把锁释放供其他使用。
如果多个线程要调用多个对象(一个线程需要同时占用多个资源,但没有拿到全部,剩下的被其他线程占用,都在等自己还需要的资源,都在等资源所以就造成了死锁),则在上锁的时候可能会出现“死锁”。
死锁产生的4个必要条件:
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结 束。 2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU 的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享 资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成 资源浪费。使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到 的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用 的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比 较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这 不仅会延长进程的周转周期,还会影响系统的吞吐量。
c、破坏“循环等待”条件 可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只 能申请编号大于i的资源。
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//创建互斥锁并初始化
pthread_mutex_lock(&mutex);//对线程上锁,此时其他线程阻塞等待该线程释放锁
//do something here
pthread_mutex_unlock(&mutex);//执行完后释放锁
创建一个线程
int pthread_start(int arg_fd)
{
struct _ctr_pthread ctr_pthread;
struct _ctr_pthread *p_arg=&ctr_pthread;
pthread_attr_t attr;
pthread_t tid;
p_arg->arg_fd=arg_fd;
p_arg->p_over=0;
//pthread_attr_t thread_attr;
if( pthread_attr_init(&attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
goto CleanUp;
}
if( pthread_attr_setstacksize(&attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));//线程栈的大小
goto CleanUp;
}
if( pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));//线程属性分离
goto CleanUp;
}
printf("pthread_set ok!\n");
if(pthread_create(&tid,&attr,datadell,(void *)p_arg) < 0)
{
printf("create a pthread failure:%s\n",strerror(errno));
goto CleanUp;
}
printf("there create a new pthread to dell data.\n");
CleanUp:
pthread_attr_destroy(&attr);
return -1;
}
服务器多并发处理为了减少创建线程的时间会用线程池(还不会)