1.进程状态
1.1 孤儿进程
父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init
进程会循环地 wait() 它的已经退出的子进程。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
pid_t pid = - 1 ;
pid = fork ( ) ;
if ( - 1 == pid)
{
perror ( "fork" ) ;
return 1 ;
}
if ( pid > 0 )
{
printf ( "父进程休息1s后退出\n" ) ;
sleep ( 1 ) ;
printf ( "父进程退出\n" ) ;
exit ( 0 ) ;
}
while ( 1 )
{
printf ( "子进程工作,ppid = %d\n" , getppid ( ) ) ;
sleep ( 1 ) ;
}
return 0 ;
}
1.2 僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进
程。
这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保
留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有
限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进
程,此即为僵尸进程的危害,应当避免。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
pid_t pid = - 1 ;
pid = fork ( ) ;
if ( - 1 == pid)
{
perror ( "fork" ) ;
return 1 ;
}
if ( pid == 0 )
{
for ( int i = 0 ; i< 5 ; i++ )
{
printf ( "子进程运行:%d\n" , i) ;
sleep ( 1 ) ;
}
printf ( "子进程结束\n" ) ;
exit ( 0 ) ;
}
sleep ( 100 ) ;
printf ( "父进程睡醒了,结束\n" ) ;
return 0 ;
}
1.3 进程替换
exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调
用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
printf ( "helloworld!\n" ) ;
execl ( "/bin/ls" , "ls" , "-l" , "/home" , NULL ) ;
printf ( "hello execlp\n" ) ;
return 0 ;
}
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
char * argvs[ ] = { "ls" , "-l" , "/home" , NULL } ;
char * envp[ ] = { "ADDR = XIAAN" , NULL } ;
printf ( "hello wolrd\n!" ) ;
execvpe ( "ls" , argvs, envp) ;
printf ( "hello execle!\n" ) ;
return 0 ;
}
其中只有 execve() 是真正意义上的系统调用,其它都是在此基础上经过包装的
库函数。
进程调用一种 exec 函数时,该进程完全由新程序替换,而新程序则从其 main
函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID (当然还有父
进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换
了当前进程的正文、数据、堆和栈段(进程替换)。
补充说明:
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址
exec 函数族与一般的函数不同,exec 函数族中的函数执行成功后不会返回,而
且,exec 函数族下面的代码执行不到。只有调用失败了,它们才会返回 -1,失败后
从原程序的调用点接着往下执行
2.进程间通信概念
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)
之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要
进程间通信
进程间通信的目的:
1.数据传输:一个进程需要将它的数据发送给另一个进程。
2.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某
种事件(如进程终止时要通知父进程)。
3.资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和
同步机制。
4.进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控
制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2.1 无名管道
2.1.1 概述
管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的
UNIX 系统都支持这种通信机制。
管道有如下特点:
1) 半双工,数据在同一时刻只能在一个方向上流动。
2) 数据只能从管道的一端写入,从另一端读出。
3) 写入管道中的数据遵循先入先出的规则。
4) 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
5) 管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
6) 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
7) 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
8) 管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
2.1.2 pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建无名管道。
参数:
pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。
返回值:
成功:0
失败:-1
**可以使用ulimit -a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。**
2.1.3 查看管道缓冲区函数
#include <unistd.h>
long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:
fd:文件描述符
name:
_PC_PIPE_BUF,查看管道缓冲区大小
_PC_NAME_MAX,文件名字字节数的上限
返回值:
成功:根据name返回的值的意义也不同。
失败: -1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
int fds[ 2 ] ;
int ret = - 1 ;
ret = pipe ( fds) ;
if ( - 1 == ret)
{
perror ( "pipe" ) ;
return 1 ;
}
printf ( "fds[0]:%d fds[1]:%d\n" , fds[ 0 ] , fds[ 1 ] ) ;
printf ( "pipe read size:%ld\n" , fpathconf ( fds[ 0 ] , _PC_PIPE_BUF) ) ;
printf ( "pipe write size:%ld\n" , fpathconf ( fds[ 1 ] , _PC_PIPE_BUF) ) ;
close ( fds[ 0 ] ) ;
close ( fds[ 1 ] ) ;
return 0 ;
}
2.1.4 管道的读写特点
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
管道写端被全部关闭,read返回0 (相当于读到文件结尾)
写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
写管道:
管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
管道读端没有全部关闭:
管道已满,write阻塞。
管道未满,write将数据写入,并返回实际写入的字节数。
2.1.5 设置为非阻塞的方法
设置方法:
//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
结论: 如果写端没有关闭,读端设置为非阻塞, 如果没有数据,直接返回-1。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main ( )
{
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int ret = - 1 ;
int fds[ 2 ] ;
char buffer[ 64 ] ;
pid_t pid = - 1 ;
ret = pipe ( fds) ;
if ( - 1 == ret)
{
perror ( "pipe" ) ;
return 0 ;
}
pid = fork ( ) ;
if ( - 1 == pid)
{
perror ( "fork" ) ;
return 1 ;
}
if ( 0 == pid)
{
close ( fds[ 1 ] ) ;
memset ( buffer, 0 , 64 ) ;
printf ( "子进程读管道内容。。\n" ) ;
ret = fcntl ( fds[ 0 ] , F_GETFL) ;
ret | = O_NONBLOCK;
fcntl ( fds[ 0 ] , F_SETFL, ret) ;
ret = read ( fds[ 0 ] , buffer, 64 ) ;
if ( ret < 0 )
{
perror ( "rear" ) ;
exit ( - 1 ) ;
}
printf ( "son process :%s\n" , buffer) ;
close ( fds[ 0 ] ) ;
exit ( 0 ) ;
}
close ( fds[ 0 ] ) ;
sleep ( 1 ) ;
memset ( buffer, 0 , 64 ) ;
write ( fds[ 1 ] , "ABCDEFG" , 7 ) ;
if ( ret < 0 )
{
perror ( "write" ) ;
return 1 ;
}
printf ( "Father process:%s\n" , buffer) ;
close ( fds[ 1 ] ) ;
return 0 ;
}
2.2 有名管道
2.2.1 概述
管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了
命名管道(FIFO),也叫有名管道、FIFO文件。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,
以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在
亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此
,通过 FIFO 不相关的进程也能交换数据。
命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:
1) FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在
内存中。
2) 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3) FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
2.2.2 有名管道创建
可以通过命令来创建:mkfifo fifo
通过函数来创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:
命名管道的创建。
参数:
pathname : 普通的路径名,也就是创建后 FIFO 的名字。
mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
返回值:
成功:0 状态码
失败:如果文件已经存在,则会出错且返回 -1。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main ( )
{
int ret = - 1 ;
ret = mkfifo ( "fifo" , 0644 ) ;
if ( - 1 == ret)
{
perror ( "mkfifo" ) ;
return 1 ;
}
printf ( "创建管道成功。。\n" ) ;
return 0 ;
}
2.2.3 有名管道读写操作
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数
都可用于fifo。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是
从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等
文件定位操作。
读操作read.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int fd = - 1 ;
int ret = - 1 ;
char buf[ 128 ] ;
fd = open ( "fifo" , O_RDONLY) ;
if ( - 1 == fd)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只读的方式打开一个管道。。\n" ) ;
while ( 1 )
{
memset ( buf, 0 , 128 ) ;
ret = read ( fd, buf, 128 ) ;
if ( ret <= 0 )
{
perror ( "read" ) ;
break ;
}
printf ( "buf:%s\n" , buf) ;
}
close ( fd) ;
return 0 ;
}
写操作write.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int i = 0 ;
int fd = - 1 ;
int ret = - 1 ;
char buf[ 128 ] ;
fd = open ( "fifo" , O_WRONLY) ;
if ( - 1 == fd)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只写的方式打开一个管道。。\n" ) ;
while ( 1 )
{
memset ( buf, 0 , 128 ) ;
sprintf ( buf, "hello world %d" , i++ ) ;
ret = write ( fd, buf, strlen ( buf) ) ;
if ( ret <= 0 )
{
perror ( "write" ) ;
break ;
}
printf ( "write fifo:%d\n" , ret) ;
sleep ( 1 ) ;
}
close ( fd) ;
return 0 ;
}
2.2.4 有名管道注意事项
1) 一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道
2)一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道
读管道:
1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
3.管道写端被全部关闭,read返回0 (相当于读到文件结尾)
4.写端没有全部被关闭,read阻塞等待
写管道:
1.管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
2.管道读端没有全部关闭:
3. 管道已满,write阻塞。
4. 管道未满,write将数据写入,并返回实际写入的字节数。
2.2.5 案例:不同的进程之间进行通讯
用双通道进行通讯
进程A:先读后写
以只读的方式打开管道1 以只写的方式打开管道2
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int fdr = - 1 ;
int fdw = - 1 ;
int ret = - 1 ;
char buffer[ 128 ] ;
fdr = open ( "fifo1" , O_RDONLY) ;
if ( - 1 == fdr)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只读的方式打开管道1..\n" ) ;
fdw = open ( "fifo2" , O_WRONLY) ;
if ( - 1 == fdw)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只写的方式打开管道2..\n" ) ;
while ( 1 )
{
memset ( buffer, 0 , 128 ) ;
ret = read ( fdr, buffer, 128 ) ;
if ( ret <= 0 )
{
perror ( "read" ) ;
break ;
}
printf ( "read:%d %s\n" , ret, buffer) ;
memset ( buffer, 0 , 128 ) ;
fgets ( buffer, 128 , stdin ) ;
if ( 0 == buffer[ strlen ( buffer) - 1 ] )
buffer[ strlen ( buffer) - 1 ] = 0 ;
ret = write ( fdw, buffer, strlen ( buffer) ) ;
if ( ret <= 0 )
{
perror ( "write" ) ;
break ;
}
printf ( "write:%d\n" , ret) ;
}
close ( fdr) ;
close ( fdw) ;
return 0 ;
}
进程B:先写后读
以只读的方式打开管道2 以只写的方式打开管道1
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int fdr = - 1 ;
int fdw = - 1 ;
int ret = - 1 ;
char buffer[ 128 ] ;
fdw = open ( "fifo1" , O_WRONLY) ;
if ( - 1 == fdw)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只写的方式打开管道1..\n" ) ;
fdr = open ( "fifo2" , O_RDONLY) ;
if ( - 1 == fdr)
{
perror ( "open" ) ;
return 1 ;
}
printf ( "以只读的方式打开管道2..\n" ) ;
while ( 1 )
{
memset ( buffer, 0 , 128 ) ;
fgets ( buffer, 128 , stdin ) ;
if ( '\n' == buffer[ strlen ( buffer) - 1 ] )
buffer[ strlen ( buffer) - 1 ] = '\0' ;
ret = write ( fdw, buffer, strlen ( buffer) ) ;
if ( ret <= 0 )
{
perror ( "write" ) ;
break ;
}
printf ( "write:%d\n" , ret) ;
memset ( buffer, 0 , 128 ) ;
ret = read ( fdr, buffer, 128 ) ;
if ( ret <= 0 )
{
perror ( "read" ) ;
break ;
}
printf ( "read:%d %s\n" , ret, buffer) ;
}
close ( fdr) ;
close ( fdw) ;
return 0 ;
}
3. 共享存储映射
3.1 概述
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区
相映射。
于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入
缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况
下,使用地址(指针)完成I/O操作。
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以
直接读写内存,而不需要任何数据的拷贝。
3.2 存储映射函数
(1) mmap函数
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
一个文件或者其它对象映射进内存
参数:
addr : 指定映射的起始地址, 通常设为NULL, 由系统指定
length:映射到内存的文件长度
prot: 映射区的保护方式, 最常用的 :
a) 读:PROT_READ
b) 写:PROT_WRITE
c) 读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
a) MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
关于mmap函数的使用总结:
1) 第一个参数写成NULL
2) 第二个参数要映射的文件大小 > 0
3) 第三个参数:PROT_READ 、PROT_WRITE
4) 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
5) 第五个参数:打开的文件对应的文件描述符
6) 第六个参数:4k的整数倍,通常为0
(2) munmap函数
#include <sys/mman.h>
int munmap(void *addr, size_t length);
功能:
释放内存映射区
参数:
addr:使用mmap函数创建的映射区的首地址
length:映射区的大小
返回值:
成功:0
失败:-1
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
int main ( )
{
int fd = - 1 ;
int ret = - 1 ;
void * addr = NULL ;
fd = open ( "11txt" , O_RDWR) ;
if ( - 1 == fd)
{
perror ( "open" ) ;
return 1 ;
}
addr = mmap ( NULL , 1024 , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ) ;
if ( addr == MAP_FAILED)
{
perror ( "mmap" ) ;
return 1 ;
}
printf ( "文件映射完成..\n" ) ;
close ( fd) ;
memcpy ( addr, "1234567890" , 10 ) ;
munmap ( addr, 1024 ) ;
return 0 ;
}
3.3 注意事项
1) 创建映射区的过程中,隐含着一次对映射文件的读操作。
2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保
护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3) 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
4) 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要
有实际大小。mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引
起的。
5) munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
6) 如果文件偏移量必须为4K的整数倍。
7) mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
3.4 共享映射的方式操作文件
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main ( )
{
int fd = - 1 ;
int ret = - 1 ;
void * addr = NULL ;
pid_t pid = - 1 ;
fd = open ( "11txt" , O_RDWR) ;
if ( - 1 == fd)
{
perror ( "open" ) ;
return 1 ;
}
addr = mmap ( NULL , 1024 , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ) ;
if ( addr == MAP_FAILED)
{
perror ( "mmap" ) ;
return 1 ;
}
printf ( "文件映射完成..\n" ) ;
close ( fd) ;
pid = fork ( ) ;
if ( - 1 == pid)
{
perror ( "fork" ) ;
return 1 ;
}
if ( 0 == pid)
{
memcpy ( addr, "ABCDEFGHIJ" , 10 ) ;
}
else
{
wait ( NULL ) ;
printf ( "addr:%s\n" , ( char * ) addr) ;
}
munmap ( addr, 1024 ) ;
return 0 ;
}
3.5 共享映射实现父子进程通信
int fd = open ( "xxx.txt" , O_RDWR) ;
int len = lseek ( fd, 0 , SEEK_END ) ;
void * ptr = mmap ( NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ) ;
if ( ptr == MAP_FAILED)
{
perror ( "mmap error" ) ;
exit ( 1 ) ;
}
close ( fd) ;
pid_t pid = fork ( ) ;
if ( pid == 0 )
{
sleep ( 1 ) ;
printf ( "%s\n" , ( char * ) ptr) ;
}
else if ( pid > 0 )
{
strcpy ( ( char * ) ptr, "i am u father!!" ) ;
wait ( NULL ) ;
}
int ret = munmap ( ptr, len) ;
if ( ret == - 1 )
{
perror ( "munmap error" ) ;
exit ( 1 ) ;
}