多进程编程
1.多进程
一个程序的执行活动,就是一个进程,系统为这个进程分配独立的地址空间,资源等等,所以进程事实上就是一个资源的集合体。进程就是为多道编程服务的,通过系统的调度,使得系统可以执行多个进程,使得多个进程看起来都可以同时被系统执行。
多进程编程主要的内容包括进程的控制和进程间的通信。
1.1 进程的控制
1.1.1 进程的创建
pid_t fork(void)
fork()被调用一次,却返回两次,可能的返回值:
1.在父进程中,fork 返回新创建的子进程的的 PID
2.在子进程中,fork 返回零
3.如果出现错误, fork 返回一个负值
子进程被创建之后,会从 fork()之后的代码开始运行,而不会重复运行父进程运行过的代码,
-
1 int main(int argc,char*argv) 2 { 3 pid_t pid; 4 pid = fork();// 创建进程之后,父子进程,都是从这边开始去执行代码 5 if(pid <0) 6 printf("error\n"); 7 elseif(pid ==0) 8 printf("child PID = %d\n", getpid()); 9 else 10 printf(" PPID = %d\n", getppid()); 11 return0; 12 }
fork() 运行的时候,其实会返回两次数值,父进程返回一次,子进程返回一次。程序运行到 fork() 函数的时候,全部的代码会被共享一份出来,变成两份代码一起运行,也就是说代码会被共享。而数据的部分,是从父进程拷贝一份出来给子进程,也就是说数据是拷贝。之后的运行,各个进程都有自己独立的地址空间和资源,就各归各的,以后他们要进行通信的话,就只能是实现进程间的通信了。
一般来说,因为对于父进程,fork() 返回的是子进程的 pid,而子进程返回的是零,返回值的不同,所以我们一般都是通过 if/...... less 的方式,实现父子进程执行不同的代码,完成不同的功能。
pid_t vfork(void)也是创建子进程,
区别:
数据段的区别:
vfork():子进程与父进程共享父进程的数据段
fork: 子进程拷贝父进程的数据段
执行数序的区别:
vfork : 子进程先运行,子进程运行 exec 函数族的时候或者 exit 的时候,才运行父进程
fork : 父子进程的顺序不确定
vfork 和fork,都创建了进程,但是 vfork 一般的主要目的是 使用 exex 函数族执行其他的程序,实际上,在没有调用 exec 和 exit 子函数之前,子进程与父进程是共享数据段的。vfork 运行,子进程先运行,父进程被挂起,知道子进程调用 exec 或者 exit 。父子进程就不再有限制。
相同:
两者都是被调用一次,返回两次。子进程的返回值是零,父进程的返回值是子进程的的进程ID,
1.1.2 exec 函数族
fork 是创建一个新的进程,产生一个新的 PED,但是 exec 则是启动一个新的进程,然后抵换原有的进程,所以进程的 PED 是不会被改变。
-
1 int execl(constchar*path,constchar*arg1,,,,) 2 path :被指定的程序名字,包含完整的路径 3 arg1 - argn 被指定的程序所需的命令行的参数,以空指针(NULL)结束。
-
1 int execlp(constchar*path,constchar*arg1,,,,) 2 path :被执行的程序名,这个是不包含路径的哦, 3 argv :被指定的命令行参数,还是以空指针,NULL ,结束
-
1 int execlv(constchar*path,constchar*argv[]) 2 path :被执行的程序名,包含完整的路径 3 argv :被指定的命令行参数,是包含在数组里面
-
1 int system(constchar*string ) 2 调用 fork 产生子进程,由子进程来调用/bin/sh -c string 来执行 string 所代表的命令
1.1.3 进程等待
pid_t wait(int*status )
阻塞该进程,让它一直等待,知道他的某个子进程退出,
1 int main(int argc,char**argv) 2 { 3 pid_t pc, pr; 4 // 创建进程 5 pc = fork(); 6 if(0== pc) 7 { 8 printf("这是子进程,进程号是 : %d\n", getpid()); 9 sleep(10); 10 } 11 elseif(pc >0) 12 {// 保证是子进程运行之后,再运行父进程, 13 pr = wait(NULL); 14 printf("这是父进程,进程号是 : %d\n", getppid()); 15 } 16 return0; 17 }
只要出现了 wait ,那么进程就会被阻塞,直到子进程退出之后,再运行父进程
1.2 进程间通信
任务的完成不是由一个单一的进程完成的,所以必须进行通信。
通信的方式:
(1)管道
(2)信号
(3)消息队列
(4)共享内存
(5)信号量
(6)套接字
1.2.1 管道
管道是进程间通信最古老的方式,分为无名管道和有名管道。
A. 无名管道
用于父子进程间的通信,
1 创建管道: 2 int pipe(int filedis[2]);
管道被创建,会返回来两个文件的描述符。 fileis[0] 用于读管道,fileis[1]用于写管道。无名管道被用于父子进程之间的通信,所以,必须是先创建一个管道,然后在 fork 函数创建进程,这样子进程也就会完全进程父进程所创建的管道,所以必须记住,先创建管道,再创建进程。
1 #define INPUT 1 2 #define OUTPUT 0 3 4 void main(){ 5 int file_descriptors[2]; 6 /*定义子进程号 */ 7 pid_t pid; 8 char buf[256]; 9 int returned_count; 10 /*创建无名管道*/ 11 pipe(file_descriptors); 12 /*创建子进程*/ 13 if((pid = fork())==-1){ 14 printf("Error in fork/n"); 15 exit(1); 16 } 17 /*执行子进程*/ 18 if(pid ==0){ 19 printf("in the spawned (child) process.../n"); 20 /*子进程向父进程写数据,关闭管道的读端*/ 21 close(file_descriptors[INPUT]); 22 write(file_descriptors[OUTPUT],"test data", strlen("test data")); 23 exit(0); 24 } 25 else{ 26 /*执行父进程*/ 27 printf("in the spawning (parent) process.../n"); 28 /*父进程从管道读取子进程写的数据,关闭管道的写端*/ 29 close(file_descriptors[OUTPUT]); 30 returned_count = read(file_descriptors[INPUT], buf,sizeof(buf)); 31 printf("%d bytes of data received from spawned process: %s/n", 32 returned_count, buf); 33 } 34 }
B. 有名管道
用于任意两个进程之间的通信
1 int mkfifo(constchar*pathname,mode_t mode) 2 pathname : FIFO 文件名,实质上,就是文件名嘛。 3 mode :属性, 4 实质上,命名管道,就是一个文件,两个进程之间的通信,实质就是通过一个文件,进行通信
明明管道,本质上就是一个文件,所以创建了文件之后,就可以使用文件的 read write open 对文件进行操作了。
read.c: #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() { char buf[100]; int fd; int by; // 创建管道 mkfifo("FIFO", O_CREAT | O_RDONLY); // 清零 memset(buf,0,sizeof(buf)); // 打开管道,其实就是文件拉, fd = open("FIFO", O_RDONLY | O_NONBLOCK); if(fd ==-1) { printf(" failed to open\n"); exit(-1); } while(1) {// 循环,读取,循环清零 memset(buf,0,sizeof(buf)); if((by = read(fd, buf,100))==-1) { printf("no data\n "); } printf("read %s \n", buf); sleep(1); } return0; } write.c: #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main(int argc,char*argv) { char buf[100]; int fd; int by; if(argc <2) { printf("give moer inf \n"); exit(-1); } // 清零 memset(buf,0,sizeof(buf)); // 打开管道,其实就是文件拉, fd = open("FIFO", O_RDONLY | O_NONBLOCK); if(fd ==-1) { printf(" failed to open\n"); exit(-1); } // 将输入的字符串,拷贝给 buf strcpy(bufm argv[1]); if((by = write(fd, buf,100))==-1) { printf("error\n"); } else { printf("FIFO is %s \n", buf); } return0; }
1.2.2 信号
信号的机制是 Linux 最为古老的方式,
信号的发送:
1 int kill(pid_t pid,int signo) 2 kill 可以给自己,也可以给其他的进程发送信号 3 pid :指定发送信号的进程号,就把 signo 发送给指定的进程
int raise(int signo) raise 只能给进程的自身发送信号,自己给自己发,所以不需要 PID
1 unsignedint alarm(unsignedint seconds) 2 指定了当经过 seconds 秒之后,就会自己给自己发送一个 SIFALRM 信号 3 4 int pause(void) 5 使进程等待,一直等到接收到一个信号为止,才不在等待 6 7 信号的处理 8 接收到信号,要么忽略,要么按照指定的函数执行,要么就按照默认的方式去进行处理
1 void(*signal(int signo,void(*func)(int)))(int) 2 func : 3 SIG_IGN :忽略信号 4 SIG_DFL :按照默认的方式去处理 5 信号处理的函数名:指定函数,传入的参数,就是信号比如 SIGINT 6 返回值:还是一个函数的指针, 7 void func(int sign_no) 8 { 9 if(sign_no == SIGINT) 10 printf("signal is SIGINT\n"); 11 12 if(sign_no == SIGQUIT) 13 printf("signal is SIGQUIT\n"); 14 15 } 16 int main(int argc,char** argv) 17 { 18 // 注册信号处理的函数 19 signal(SIGINT, func); 20 signal(SIGQUIT, func); 21 // 注册好之后,会一直等待,知道有参数进来 22 pause(); 23 return0; 24 }
kill - s +信号+ PID
1.2.3 消息队列
和滚到很类似,这个是快被淘汰的方式。
1.2.4 共享内存
共享内存是运行在同一台机器上最快的通信的方式,因为数据不需要不同的进程间的复制。本质上,是一个进程创建一个共享的内存,然后其他的进程则对这块内存进行读写。
A. 创建内存
int shmget(key_t key,int size,int shmflg) key:内存创建的标志,0/ IPC_PRIVATE,IPC_PRIVATE,表示创建新的共享的内存, size :创建的大小,以字节为单位 成功创建的话,返回内存的标识符,错误就为-1
内存的创建和 malloc 非常的类似
B. 映射内存
int shmat(int shmid,char*shmaddr,int flag ) shmid :是shmget 函数返回的光纤内存的标识符 flag :一般是零 shmaddr :获取值,获得共享内存的地址,一般为零,让系统分配 成功的话,返回地址,失败的话,返回值为-1
C.解除内存
char *shmdt(char*shmaddr) char*c_addr,*p_addr; int shmid; // 分配内存 shmid = shmget(IPC_PRIVATE,1024, S_IRUSR | S_IWUSR); if(fork()) {// 获得映射的地址 p_addr = shmat(shmid,0,0); memset(p_addr,0,1024); strncpy(p_addr, argv[1],1024); // 等待,子进程结束 wait(NULL); exit(0); } else {// 保证父进程先执行 sleep(2); // 获得映射的地址 c_addr = shmat(shmid,0,0) printf(" get %s \n", c_addr); }
1.2.5 套接字
这个部分,是使用 socket 编程的套接字。
1.2.6 信号量
5.信号量
信号量,又叫信号灯,实质上就是一个计数器,与原子的操作类似。用于为多个线程提供共享数据对象的访问。保证了共享资源对象,会被有限调用。
执行的操作:
(1)设置信号量,这个信号量的值自己设定,值为最多可以接收访问的最大值,非负值
(2)当一个线程访问的时候,只要信号量的值不为零,那么就可以被访问,此时,信号量的值就减一,没来一次访问就减一;当信号量的值为零,那么进程就会进入休眠;
(3)当一个线程访问完毕之后,就要释放信号量,也就是信号量的值就加一,这样以前被休眠的进程,就会被唤醒,
信号量的操作:
所以信号量在创建的时候,就必须设置一个非零的初始值,表示同时有几个线程访问该信号量保护起来的共享的资源,当初始值为一的时候,就变成互斥锁了,关于互斥锁的资料,后面补齐。,互斥锁的话,每次只能有一个线程访问资源。
信号量的API(内核):
(1)信号量的初始化
DEFINE_SEMAPHORE(semaphore_lock)// 一步完成信号量的定义和初始化,此时的信号量设置为1,也就是为互斥锁
// 也可以,分开来,不过比较麻烦,不过是一样的作用
staticstruct semaphore sem;// 信号量的定义
sema_init(struct semaphore * sem,int val);// 信号量的初始化
(2)获得信号量
void down(struct semaphore *sem)// 获得信号量,其实做减一,当为零的时候,进程就好休眠
int down_interruptible(struct semaphore * sem)
int down_trylock(struct semaphore * sem)// 三种获得信号量的区别,这里不做详解
(3)释放信号量
void up(struct semaphore *sem)// 其实就是做加一的动作,将信号量的值加一