系列文章目录
文章目录
前言
- 学习进程的产生方式,fork()、system()、exec()函数等
- Linux进程间的通信和同步方式,包括管道pipe、命名管道fifo、信号量sem、共享内存shm、消息队列msg、以及信号signal。对其进行学习记录
- 对Linux下线程编程方式进行学习,包括互斥、条件变量、线程信号等。
一、Linux 进程
Linux的进程操作方式主要有产生进程、终止进程,并且进程之间的通信和同步。
1.1 进程的产生过程
- 首先复制父进程的环境变量
- 在内核中建立进程结构
- 将结构插入进程列表,便于维护
- 分配资源给此进程
- 复制父进程的内存映射信息
- 管理文件描述符和链接点
- 通知父进程
1.2 进程的终止方式
- 从main返回
- 调用exit
- 调用_exit
- 调用abort
- 有一个信号终止
进程在终止的时候,系统会释放进程所拥有的资源,例如内存、文件和内和结构等。
1.3 进程间的通信
进程间的通信常见的有管道、共享内存、消息队列。
- 管道:与文件的操作类似,仅仅在管道的一端只读,另一端只写,利用读写的方式在进程之间传递数据
- 共享内存:内存中的一段地址在多个进程间共享,多个进程利用获得的共享内存地址对内存进行操作
- 消息队列:在内核建立一个链表
1.4 进程间的同步
进程间同步的方式只要有消息队列、信号量等。
信号量是一个共享的表示数量的值,用于对个进程之间操作或者共享资源的保护
进程和线程的区别和联系
- 进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间,进程系统资源分配的时候,除了CPU资源外,不会给线程分配独立的资源线程所需要的资源需要共享。
- 线程是进程的一部分,如果进程没有显示线程分配,可以认为进程是单线程,如果进程中建立了线程,可以认为系统为多线程
- 多线程和多进程是不同的概念。虽然二者都是并行完成功能,但是多线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。
二、进程的产生方式
2.1 进程号
每个进程在初始化的时候,系统都分配一个ID号用于标识此进程,在Linux中进程号是唯一的,系统可以用这个值表示一个进程,描述进程的ID号通常叫做PID
PID的变量类型为pid_t.
2.1.1 getpid() getppid()函数
getpid()是获取当前的进程ID号,getppid()是返回当前进程父进程的ID号,类型为pid_t
int main(int argc, char **argv)
{
pid_t pid,ppid;
pid = getpid();
ppid = getppid();
printf("pid = %d ppid = %d\n", pid, ppid);
return 0;
}
2.2 进程复制fork()
fork()函数以父进程为蓝本复制一个进程,其ID号和父进程的ID号不同,在Linux环境下 fork()是以写复制实现的,只有内存等与父进程不同时,其他与父进程共享,只有在父进程或者子进程进行修改后才重新生成一份。以下为例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char **argv)
{
pid_t pid;
pid = fork();
if(-1 == pid)
{
printf("Error:fork\n");
return -1;
}else if(0 == pid){
printf("child process PID = %d parent PID = %d\n", getpid(),getppid());
}else{
printf("parent process PID = %d parent PID = %d\n", getpid(),getppid());
}
return 0;
}
wsj@realarm:~/work/net_stu$ ./a.out
parent process PID = 13851 parent PID = 6537
child process PID = 13852 parent PID = 13851
2.3 system() 方式
system()函数调用shell的外部命令在当前进程中开始另一个进程。
system()函数调用“/bin/sh-c command”执行特定的命令,阻塞当前进程知道command命令执行完毕
system()函数原型:
#include <stdlib.h>
int system(const char *command);
执行system()函数时会调用fork() execve() waitpid()等函数。其中任意一个调用失败,将导致system()函数调用失败,
进程执行exec()函数
在使用fork()函数和system()函数的时候,系统都会建立一个新的进程,执行调用者的操作。而原来的进程还用存在,知道用户显式的退出, exec()族函数会用新的进程替换原有的进程,系统会从新的进程运行,新的进程PID值与原来的进程PID值相同。exec()
extern int execve (const char *__path, char *const __argv[],
char *const __envp[]) __THROW __nonnull ((1, 2));
#ifdef __USE_XOPEN2K8
/* Execute the file FD refers to, overlaying the running program image.
ARGV and ENVP are passed to the new program, as for `execve'. */
extern int fexecve (int __fd, char *const __argv[], char *const __envp[])
__THROW __nonnull ((2));
#endif
/* Execute PATH with arguments ARGV and environment from `environ'. */
extern int execv (const char *__path, char *const __argv[])
__THROW __nonnull ((1, 2));
/* Execute PATH with all arguments after PATH until a NULL pointer,
and the argument after that for environment. */
extern int execle (const char *__path, const char *__arg, ...)
__THROW __nonnull ((1, 2));
/* Execute PATH with all arguments after PATH until
a NULL pointer and environment from `environ'. */
extern int execl (const char *__path, const char *__arg, ...)
__THROW __nonnull ((1, 2));
/* Execute FILE, searching in the `PATH' environment variable if it contains
no slashes, with arguments ARGV and environment from `environ'. */
extern int execvp (const char *__file, char *const __argv[])
__THROW __nonnull ((1, 2));
/* Execute FILE, searching in the `PATH' environment variable if
it contains no slashes, with all arguments after FILE until a
NULL pointer and environment from `environ'. */
extern int execlp (const char *__file, const char *__arg, ...)
__THROW __nonnull ((1, 2));
上述函数中,只有execve()函数是真正意义上的系统调用,其他都是在此基础上包装的库函数,上述函数的作用是在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用他来取代调用进程的内容。与fork()函数不同,exec()函数族的函数执行成功后不会返回,这是因为执行新的程序已经占用了当前进程的空间和资源,这些资源包括代码段、数据段和堆栈等,他们都已经被新的内容取代而进程的ID等标示性的信息仍然是原来的东西,即exec(0函数族在原来的壳上运行了自己的程序,只有程序调用失败了系统才会返回-1.
进程间通信和同步
在Linux下多进程间的通信机制叫做IPC ,他是多个进程之家相互沟通的一种方法,早Linux下有多种进程间通信的方法:半双工管道,FIFO(命名管道)、消息队列、信号量、共享内存等。
半双工管道pipe
管道是一种把两个进程之间的标准输入和标准输出连接在一起的机制,因此称为半双工;在shell中管道用“|” 表示,例如:
ls -l | grep *.c
通过代码的方式: 创建管道的函数原型:pipe()
#include <unistd.h>
int pipe(int filedes[2);
/*数组filedes是一个文件描述符的数组,用于保存管道返回的两个文件描述符
数组中的第一个元素是为了读操作而创建打开的,第二个元素是为了写操作创建打开的,
即 fd1的输出是fd0的输入,当函数执行成功是返回0,执行失败返回-1*/
建立管道代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
int main(void)
{
int result = -1;
int fd[2], nbytes;
pid_t pid;
char string[] = "你好,管道";
char readbuffer[80];
/*文件描述符 1 用于写 0 用于读*/
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(-1 == result){
printf("pipe error\n");
return -1;
}
pid = fork();
if(-1 == pid){
printf("fork error\n");
return -1;
}else if(0 == pid){
close(*read_fd);
result = write(*write_fd,string,strlen(string));
return 0;
}else{
close(*write_fd);
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
printf("read buf :%s\n",readbuffer);
}
return 0;
}
管道阻塞和管道操作的原子性
当管道的写端没有关闭时,如果写请求的字节数大于阀值PIPE_BUF,写操作返回值是管道中目前的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有的字节数(此时管道中数据量小于请求的数据量)或者返回请求的字节数(此时管道中的数据量不小于请求的数据量)
管道进行写入操作的时候,当写入数据的数目小于128K时写入时非原子性的,如果把父进程中的两次写入字节数都改为28K,可以发现写入管道的数据量大于128k字节时,缓冲区的数据将被连续的写入管道,知道数据全部写完位置,如果没有进程读数据则移植阻塞。
管道操作原子性代码实例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#define K 1024
#define WRITELEN (128*K)
int main(void)
{
int result = -1;
int fd[2], nbytes;
pid_t pid;
char string[] = "你好,管道";
char readbuffer[10*K];
/*文件描述符 1 用于写 0 用于读*/
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(-1 == result){
printf("pipe error\n");
return -1;
}
pid = fork();
if(-1 == pid){
printf("fork error\n");
return -1;
}else if(0 == pid){
close(*read_fd);
int write_size = WRITELEN;
result = 0;
while(write_size >= 0){
result = write(*write_fd,string,write_size);
if(result > 0){
write_size -= result;
printf("写入%d个字节,剩余%d个数据\n",result,write_size);
}else{
printf("wait 10s\n");
sleep(10);
}
if(write_size <= 0)
break;
}
return 0;
}else{
close(*write_fd);
while(1){
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
if(nbytes <= 0){
printf("没数据写入\n");
break;
}
printf("read buf :%s\n",readbuffer);
}
}
return 0;
}
命令管道
命令管道与管道的一些明显区别:
1、在文件系统中命名管道是以设备特殊文件的形式存在的
2、不同的进程可以通过命名管道共享数据
- 创建FIFO
有许多方法可以创建命名管道,其中可以直接用shell来完成,例如
[root:/tmp]# mkfifo namedfifo
[root:/tmp]# ls -l namedfifo
prw-r--r-- 1 root root 0 Aug 4 09:03 namedfifo
- FIFO操作
对命名管道FIFO来说,IO操作与普通的管道IO操作基本是一致的,二者之间存在着主要的区别,在FIFO中,必须使用一个open()函数来显式的建立连接管道的通道,一般来说FIFO总是处于阻塞状态,也就是说如果命名管道打开了设置了读权限则读进程将一致阻塞,直到其他进程打开该FIFO并向管道中写入数据,这个阻塞动作反过来也是成立的,如果一个进程打开了一个管道写入数据,当没有进程读取数据时,写管道也是阻塞的,直到已经写入的数据被读出后才能进行写操作。如果不希望在命令管道操作中发生阻塞,可以在open()调用时使用O_NONBLOCK标志,以关闭默认的阻塞动作。
消息队列
消息队列时内核地址控件的内部链表,通过Linux内核在各个进程之间传递内容,消息顺序的发送到消息队列中,并以几种不同的方式获取,每个消息队列可以用IPC标识符唯一的进行标识,内核中的消息队列是通过IPC的标识进行区别的,不同的消息队列之间是相对独立的,每个消息队列中的消息又构成一个独立的链表。