pid_t fork(void);
fork() 创建一个新的子进程,fork() 之前的内容只在父进程中运行一次,之后的内容在父子进程之间都会运行。
在父进程中的 fork() 调用后返回的是新的子进程的 PID,子进程中的 fork() 函数调用后返回的是 0。
子进程与父进程一致的内容:
• 进程的地址空间。
• 进程上下文、代码段。
• 进程堆空间、栈空间,内存信息。
• 进程的环境变量。
• 标准 IO 的缓冲区。
• 打开的文件描述符。
• 信号响应函数。
• 当前工作路径
子进程独有的内容:
• 进程号 PID。 PID 是身份证号码,是进程的唯一标识符
• 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
• 挂起的信号。这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号,子进程也不会继承这些信号
#include
#include
#include
#include
int main(void)
{
pid_t result;
printf("This is a fork demo!\n\n");
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1) {
printf("Fork error\n");
}
/*返回值为 0 代表子进程*/
else if (result == 0) {
printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
}
/*返回值大于 0 代表父进程*/
else {
printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
}
return result;
}
/**
* exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或
* 目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进
* 程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也
* 可以是 Linux 下任何可执行的脚本文件。
*
* 在 Linux 中使用 exec 函数族主要有两种情况。
* 当进程认为自己不能再为系统和用户做出任何贡献时, 就可以调用 exec 函数族中的任意一个函数让自己重生。
* 如果一个进程想执行另一个程序,那么它就可以调用 fork()函数新建一个进程,然后调用 exec 函
* 数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
*
*/
//返回-1表示错误
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[])
int execve(const char *path, char *const argv[], char *const envp[])
/*
execl()函数用于执行参数path字符串所代表的文件路径(必须指定路径),
接下来是一系列可变参数,它们代表执行该文件时传递过去的 ``argv[0]、argv[1]… argv[n]`` ,
最后一个参数必须用空指针NULL作为结束的标志。
*/
err = execl("/bin/ls", "ls", "-la", NULL);
/*
与execl的差异是,execlp()函数会从PATH环境变量所指的目录中
查找符合参数file的文件名(不需要指定完整路径)。
*/
err = execlp("ls", "ls", "-la", NULL);
/*
与execl的差异是,execle()函数会通过最后一个参数(envp)指定新程序使用的环境变量。
*/
char *envp[] = {
"/bin", NULL
};
err = execle("/bin/ls", "ls", "-la", NULL, envp);
/*
与execl的差异是,直接使用数组来装载要传递给子程序的参数/
*/
char *argv[] = {
"ls", "-la", NULL
};
err = execv("/bin/ls", argv);
/*
是execlp,execv函数的结合体
*/
char *argv[] = {
"ls", "-la", NULL
};
err = execvp("ls", argv);
/*
是execle,execv函数的结合体
*/
char *argv[] = {
"ls", "-la", NULL
};
char *envp[] = {
"/bin", NULL
};
err = execve("/bin/ls", argv, envp);
_exit() :直接通过系统调用使进程终止运行,当然,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;
exit() : 调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲”
void _exit(int status);
void exit(int status);
pid_t wait(int *wstatus);
• wait() 要与 fork() 配套出现,如果在使用 fork() 之前调用 wait(), wait() 的返回值则为-1,正常情况下 wait() 的返回值为子进程的 PID。
• 参数 wstatus 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针,但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样做),我们就可以设定这个参数为 NULL。
Linux 系统提供了关于等待子进程退出状态的一些宏定义,我们可以使用这些宏定义来直接判断子进程退出的状态:
• WIFEXITED(status) :如果子进程正常结束,返回一个非零值
• WEXITSTATUS(status):如果 WIFEXITED 非零,返回子进程退出码
• WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
• WTERMSIG(status) :如果 WIFSIGNALED 非零,返回信号代码
• WIFSTOPPED(status):如果子进程被暂停,返回一个非零值
• WSTOPSIG(status):如果 WIFSTOPPED 非零,返回一个信号代码
/* 调用 wait,父进程阻塞 */
child_pid = wait(&status); //
if(child_pid == pid)
{
if(WIFEXITED(status)) //判断是否正常结束
{
child_retcode = WEXITSTATUS(status); //WIFEXITED 非零,获取子进程退出码
}
}
只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后 fork 产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。
/** pipe.c */
/**
* ps –ef | grep ntp
*
* 进程 ps -ef 进程 grep ntp
* | |
* |---- pipe ----|
*
* 管道是 Linux 中进程间通信的一种方式。这里所说的管道主要指无名管道,它具有如下特点:
* 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。
* 它是一个半双工的通信模式,具有固定的读端和写端。
* 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read()和 write()等函数。
* 但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中。
* • 写入操作不具有原子性,因此只能用于一对一的简单通信情形。
*
* 管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符 fds[0]和 fds[1],
* 其中 fds[0]固定用于读管道,而 fd[1]固定用于写管道,这样就构成了一个半双工的通道。
* 管道关闭时只需将这两个文件描述符关闭即可,可使用普通的 close()函数逐个关闭各个文件描述符。
*
* 用户进程
* fd[0] fd[1]
* | |
* 读 写
* |--------管道--------|
*
* 父进程 子进程
* fd[0]fd[1] fd[0]fd[1]
* | |
* 读写 读写
* |---------管道---------|
*
* 函数原型:int pipe(int fd[2])
*
*/
/**
* 注意点:
* 只有在管道的读端存在时,向管道写入数据才有意义。否则,向管道写入数据的进程将收到内核传来的 SIGPIPE 信号(通常为 Broken pipe 错误)。
* 向管道写入数据时, Linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
* 父子进程在运行时,它们的先后次序并不能保证,建议使用进程之间的同步与互斥机制之后。
*/
#include
#include
#include
#include
#include
#include
#include
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2];
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 创建管道 */
if (pipe(pipe_fd) < 0)
{
printf("pipe create error\n");
exit(1);
}
/* 创建一子进程 */
if ((pid = fork()) == 0)
{
/* 子进程关闭写描述符,并通过使子进程暂停 3s 等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
//sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 阻塞读取, read() 函数读取一个关闭了写描述符的管道,那
么 read() 会返回 0*/
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent write %d bytes : '%s'\n", real_write, data);
}
/*关闭父进程写描述符*/
close(pipe_fd[1]);
/*收集子进程退出信息*/
waitpid(pid, NULL, 0);
exit(0);
}
}
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在“血缘关系”的进程,只要可以访问该命名管道文件的路径,就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作,如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中,但数据却是存在于内存中的,这点要区分开。
命名管道有以下的特征:
• 有名字,存储于普通文件系统之中。
• 任何具有相应权限的进程都可以使用 open() 来获取命名管道的文件描述符。
• 跟普通文件一样:使用统一的 read()/write() 来读写。
• 跟普通文件不同:不能使用 lseek() 来定位,原因是数据存储于内存中。
• 具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
• 遵循先进先出(First In First Out)原则,最先被写入 FIFO 的数据,最先被读出来。
命令创建命名管道:
# 执行如下命令
mkfifo test
file test
# 以下是命令输出,可以看出它是一个命名管道类型的文件
test: fifo (named pipe)
函数创建命名管道
int mkfifo(const char * pathname,mode_t mode);
mode 模式及权限参数说明:
• O_RDONLY:读管道。
• O_WRONLY:写管道。
• O_RDWR:读写管道。
• O_NONBLOCK:非阻塞。
• O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限。
• O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在。
函数返回值说明如下:
• 0:成功
• EACCESS:参数 filename 所指定的目录路径无可执行的权限。
• EEXIST:参数 filename 所指定的文件已存在。
• ENAMETOOLONG:参数 filename 的路径名称太长。
• ENOENT:参数 filename 包含的目录不存在。
• ENOSPC:文件系统的剩余空间不足。
• ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录。
• EROFS:参数 filename 指定的文件存在于只读文件系统内。
使用 FIFO 的过程中,当一个进程对管道进行读操作时:
• 若该管道是阻塞类型,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。
• 若该管道是非阻塞类型,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果FIFO 内没有数据,读函数将立刻返回 0。
使用 FIFO 的过程中,当一个进程对管道进行写操作时:
• 若该管道是阻塞类型,则写操作将一直阻塞到数据可以被写入。
• 若该管道是非阻塞类型而不能写入全部数据,则写操作进行部分写入或者调用失败
/**fifo.c */
/**
* 无名管道,它只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制, 它可以使互不相关的两个进程实现彼此通信。
* 该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。
* 不过 FIFO 是严格地遵循先进先出规则的,对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如 lseek()等文件定位操作。
* 有名管道的创建可以使用函数 mkfifo(),该函数类似文件中的 open()操作,可以指定管道的路径和打开的模式
*
* 在创建管道成功之后,就可以使用 open()、 read()和 write()这些函数了。与普通文件的开发设置一样,对于为读而打开的管道可在 open()中设置 O_RDONLY,
* 对于为写而打开的管道可在 open()中设置 O_WRONLY,在这里与普通文件不同的是阻塞问题。由于普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,
* 这里的非阻塞标志可以在 open()函数中设定为 O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行讨论。
* (1)对于读进程。
* 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。
* 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果 FIFO内没有数据,则读函数将立刻返回 0。
* (2)对于写进程。
* 若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。
* 若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败
*
* 函数原型 int mkfifo(const char *filename,mode_t mode)
* 函数传入值 filename:要创建的管道名字(包含路径)
* 函数传入值 mode:
* O_RDONLY:读管道
* O_WRONLY:写管道
* O_RDWR:读写管道
* O_NONBLOCK:非阻塞
* O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限
* O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在
* 函数返回值:
* 0:成功
* EACCESS:参数 filename 所指定的目录路径无可执行的权限
* EEXIST:参数 filename 所指定的文件已存在
* ENAMETOOLONG:参数 filename 的路径名称太长
* ENOENT:参数 filename 包含的目录不存在
* ENOSPC:文件系统的剩余空间不足
* ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录
* EROFS:参数 filename 指定的文件存在于只读文件系统内
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MYFIFO "myfifo" /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定义在于 limits.h 中*/
void fifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("***************** read fifo ************************\n");
/* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只读阻塞方式打开有名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' from FIFO\n", buff);
}
printf("***************** close fifo ************************\n");
close(fd);
exit(0);
}
void write_fifo(void)
{
int fd;
char buff[] = "this is a fifo test demo";
int nwrite;
sleep(2); //等待子进程先运行
/* 以只写阻塞方式打开 FIFO 管道 */
fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
printf("Write '%s' to FIFO\n", buff);
/*向管道中写入字符串*/
nwrite = write(fd, buff, MAX_BUFFER_SIZE);
if(wait(NULL)) //等待子进程退出
{
close(fd);
exit(0);
}
}
int main()
{
pid_t result;
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值为 0 代表子进程*/
{
fifo_read();
}
else /*返回值大于 0 代表父进程*/
{
write_fifo();
}
return result;
}