linux网络编程多进程学习

关于多进程

对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值。fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。与DOS和早期的Windows不同,Unix/Linux系统是真正实现多任务操作的系统,可以说,不使用多进程编程,就不能算是真正的Linux环境下编程。
 虽然现在已经广泛流行多线程和多路复用了,但是还是很有必要学习下多进程。

多进程是什么

什么是进程?就我理解,进程就是正在运行的程序,进程可能会用到或者改变系统资源。对于服务器来说,可能有多个正在运行的程序连接,这就是多进程,要对一个个的进程进行最快的响应而不乱了套,这就是多进程编程。

linux下进程及进程控制

Linux下一个进程在内存里有三部分的数据,就是"代码段"、“堆栈段"和"数据段”。其实学过汇编语言的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。

“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

在传统的Unix环境下,有两个基本的操作用于创建和修改进程:函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝;函数族exec( )用来启动另外的进程以取代当前运行的进程。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别,例如在Linux系统中调用vfork和fork完全相同,而在有些版本的Unix系统中,vfork调用有不同的功能。由于这些差别几乎不影响我们大多数的编程,在这里我们不予考虑。

fork()

一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。

子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的"副本",这意味着父子进程间不共享这些存储空间。
读者也许会问,如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?其实UNIX自有其解决的办法,大家知道,一般CPU都是以"页"为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,象INTEL的CPU,其一页在通常情况下是 4086字节大小,而无论是数据段还是堆栈段都是由许多"页"构成的,fork函数复制这两个段,只是"逻辑"上的,并非"物理"上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的" 页"从物理上也分开。系统在空间上的开销就可以达到最小。

UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

(1)在父进程中,fork返回新创建子进程的进程ID;

(2)在子进程中,fork返回0;

(3)如果出现错误,fork返回一个负值。

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

pid_t fork( void); // pid_t为一个宏定义
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
getpid()getppid()

getpid()用来返回当前进程的PID,getppid()用来返回当前进程的父进程PID
值得一说的是,当fork()执行之后,返回值等于零,子进程执行,大于零,父进程执行,但是返回的大于零的值为子进程的PID。也就是说,fork返回给子进程的值为零,返回给父进程的值为子进程PID,所以大于零。

exec()

exec族函数

(1)int execl(const char *path, const char *arg, ......);  
(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);  
(3)int execv(const char *path, char *const argv[]);  
(4)int execve(const char *filename, char *const argv[], char *const envp[]);  
(5)int execvp(const char *file, char * const argv[]);  
(6)int execlp(const char *file, const char *arg, ......);

当你创建一个子进程的时候,如果想让他去执行其他的可执行程序,掉用exec可以让他替换当前的代码段,数据段,和堆栈段,全部换为新的,所以子进程也就跳出当前的代码段,所以如果正常运行,exec是不会返回值的,除非有bug就会返回到原来的代码段执行。

popen()

FILE *popen(const char *command, const char *type); int pclose(FILE *stream);
第一个参数command为想要执行的命令
第二个参数type 可以为‘r’和‘w’,分别为读和写。
该函数产生一个基于管道的文件流,他会调用fork()产生一个子进程,这个进程对应着第一个参数。命令执行的结果可以从文件流中读出。

wait()
int wait(int* statloc);
int waitpid(pid_t pid, int* statloc, int options);

wait()是一个返回进程结束后状况的函数,比如说父进程运行完了,他会阻塞在wait()直到子进程终止,并回收子进程的资源,而防止僵尸进程的产生, 如果一个已经终止、但其父进程尚未对其调用wait进行善后处理(获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵尸进程(zombie)。

竞争条件(race condition)

当多个进程对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。如果在fork之后某种逻辑显示或者隐式的依赖于在fork之后是父进程还是子进程先运行,这个时候fork就成了竞争条件产生的滋生地,通常可以调用sleep(),但是进程运行先后只依赖于系统负载以及内核的调度算法。
这个时候进程等待子进程运行完就必须调用wait(),进程等待父进程就可以使用下列形式的循环

while(getppid()!=1)
	sleep(1);

这种形式的循环叫做轮询,他的问题是浪费了CPU的时间。
为了避免竞争条件和轮询,在UNIX中可以使用信号(SIGNAL)机制,也可以使用进程间通信(IPC)。
当然还有跟好的一种方式,那就是多线程处理并发。

文件锁
	flock(fd, LOCK_EX); /* 对整个文件加锁 */
    read(fd, buf, NUM);
    buf[ret] = '\0'; 
    count = atoi(buf); 
    ++count; 
    sprintf(buf, "%d", count); 
    lseek(fd, 0, SEEK_SET); 
    write(fd, buf, strlen(buf));  
    flock(fd, LOCK_UN); /* 解锁 */

文件锁可以对临界资源上锁 ,这不过临界资源在文件里面罢了,这样在一个进程对临界资源进行操作是别的进程如果想要操作就只能阻塞着知道这个进程解锁为止。

猜你喜欢

转载自blog.csdn.net/qq_40215005/article/details/88760680
今日推荐