Linux创建新进程的系统调用是fork函数。
#include#include pid_t fork(void); 返回值: 每次调用都返回2次: 1、在父进程中返回子进程PID 2、在子进程中返回0 失败时返回-1,并设置errno
fork复制当前进程,复制后子进程跟父进程具有相同的内存空间,且代码与父进程完成相同。数据的复制采用的是写时复制,即只有在任一进程对数据执行了写操作时,复制才会发生。此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数均会加1。原进程设置的信号处理函数不再对新进程起作用。
#include13.2 exec系列系统调用#include int gval = 10; int main() { pid_t pid; int val = 10; pid = fork(); if (pid == 0) //子进程处理 { val += 2; } else //父进程 { val -= 2; } if (pid == 0) printf("child proc, val = %dn", val); else printf("parent proc, val = %dn", val); return 0; } 结果: parent proc, val = 8 child proc, val = 12
fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数来执行另一个程序。当进程调用一种exec函数后,该进程的用户空间代码和数据完全被新程序替换,exec函数后的代码不会再被执行。调用exec并不创建新进程,所以调用exec前后该进程的id不变。
#includeextern char** environ; //加载一个进程,通过 路径+程序名 来加载 最后一个参数必须是NULL,标记参数的结束 int execl(const char* path, const char* arg, ...); 如:execl("/bin/ls", "ls", "-l", NULL); //借助PATH环境变量,加载一个进程 第一个参数无需给出具体的路径,只需给出函数名即可,系统会在PATH环境变量中寻找所对应的程序,如果没找到的话返回-1。最后一个参数必须是NULL,标记参数的结束 int execlp(const char* file, const char* arg, ...); 如:execlp("ls", "ls", "-l", NULL); //file, arg[0](程序本身), arg[1], //l: 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志 e: 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境 int execle(const char* path, const char* arg, ..., char* const envp[]); 如:execle("/bin/ls", "ls", "-a", NULL, NULL); //execv中希望接收一个以NULL结尾的字符串数组的指针:char *argv[] = {"ls", "-a", "-l", NULL}; int execv(const char* path, char* const argv[]); 如:execv( "/bin/ls",arg) int execvp(const char* file, char* const argv[]); 如:execvp("ls", argv); //v: 希望接收到一个以NULL结尾的字符串数组的指针 e: 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境 int execve(const char* path, char* const argv[], char* const envp[]); 如:char *const ps_argv[] = {"ps", "-o", "pid, ppid, session, tpgid, comm, NULL"}; char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execve("/bin/ps", ps_argv, ps_envp);
exec系列函数是不返回的,只有在出错时返回-1,并设置errno。如果没出错,原程序中exec调用之后的代码都不会执。exec系列函数的关系图如下:
13.3 处理僵尸进程僵尸进程:子进程结束,父进程没有回收子进程、释放子进程占用的内核资源,此子进程处于僵尸状态,被称为僵尸进程。
父进程可以调用wait或waitpid函数来回收子进程,销毁僵尸进程。
13.3.1 wait函数--阻塞#include#include //可以等待任意子进程终止 pid_t wait(int* status); 参数: status:保存子进程的退出状态信息 返回值:成功时返回子进程ID,失败时返回-1并设置errno
sys/wait.h文件中定义了几个宏来帮助解释子进程的退出状态信息:
wait函数将阻塞父进程,直到该进程的子进程,结束为止。下面是使用wait的实例:
#include13.3.2 waitpid函数--非阻塞#include #include #include int main() { int status; pid_t pid = fork(); if (pid == 0) return 3; else { printf("child PID : %dn", pid); wait(&status); if (WIFEXITED(status)) { printf("child send one : %dn", WEXITSTATUS(status)); } } return 0; } 结果: child PID : 124959 child send one : 3
#include#include pid_t waitpid(pid_t pid, int* status, int options); 参数: pid:等待终止的子进程的PID。若传-1,则与wait函数相同,可以等待任意子进程终止。 status:保存子进程的退出状态信息 options:控制waitpid函数的行为。常用值为WNONHANG,表示waitpid是非阻塞的。 返回值:成功时返回子进程ID(或0),失败时返回-1并设置errno
waitpid函数是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程正常退出,则waitpid返回该子进程的PID。
#include13.4 管道#include #include int main() { int status; pid_t pid = fork(); if (pid == 0) { sleep(5); return 100; } else { while(!waitpid(-1, &status, WNOHANG)) { sleep(3); puts("sleep 3s!"); //即使子进程没终止,也会打印这句话,因外waitpid的非阻塞性 } if (WIFEXITED(status)) { printf("child send %dn", WEXITSTATUS(status)); } } return 0; } 结果: sleep 3s! sleep 3s! child send 100
管道是父进程和子进程间通信的常用手段。
创建一根管道后,父子进程之间能够传递数据,利用的是fork调用后复制两个管道文件描述符,如下图所示:(管道和套接字一样,属于操作系统,并非属于进程的资源,因此,fork复制的并非管道,而是管道的文件描述符)
下面是利用pipe创建一跟管道进行父子进程通信的代码示例:
#include#include #define BUF_SIZE 30 int main() { int fds[2]; char str1[] = "who are you?"; char str2[] = "I'm Lee!"; char buf[BUF_SIZE] = {0}; //创建一个管道,fd[0]读,fd[1]写 pipe(fds); //对于网络通信,也可以用socketpair(AF_UNIX, SOCK_STREAM, 0, fds); pid_t pid = fork(); if (pid == 0) { write(fds[1], str1, sizeof(str1)); //发送str1 //sleep(2); read(fds[0], buf, BUF_SIZE); //读取管道内的数据 printf("child process output: %sn", buf); //打印读取的数据 } else { read(fds[0], buf, BUF_SIZE); printf("father process output: %sn", buf); write(fds[1], str2, sizeof(str2)); //sleep(2); } return 0; } 结果: child process output: who are you?
实际运行的结果跟我么想象的不一样,是因为:数据进入管道后成为无主数据,子进程先是把数据发送到管道,接着又从管道读取数据,读取的时候不会区分这个数据是谁发的,只要管道内有数据就能读。想要使结果正常,就要把代码中的sleep注释放开。
为了实现双向通信,可以创建2个管道,各自负责不同的数据流动即可:
实现代码如下:
#include#include #define BUF_SIZE 30 int main() { int fds1[2], fds2[2]; char str1[] = "who are you?"; char str2[] = "I'm Lee!"; char buf[BUF_SIZE] = {0}; //创建2个管道 pipe(fds1), pipe(fds2); pid_t pid = fork(); if (pid == 0) { write(fds1[1], str1, sizeof(str1)); read(fds2[0], buf, BUF_SIZE); printf("child process output: %sn", buf); } else { read(fds1[0], buf, BUF_SIZE); printf("father process output: %sn", buf); write(fds2[1], str2, sizeof(str2)); } return 0; } 结果: father process output: who are you? child process output: I'm Lee!
不过,管道只能用于有关联的2个进程(如父子进程)间的通信。而system V IPC(消息队列,共享内存、信号量)能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条管道。但有一种特殊的管道名为FIFO,也叫命名管道,也能用于无关联进程之间的通信,但在网络编程中使用的不多。
13.5 信号量 13.5.1 信号量原语当多个进程同时访问系统上的某个资源时,需要考虑进程的同步问题,确保任意时刻只有一个进程可以拥有对资源的独占式访问。信号量实现了这一保护机制。
信号量(semaphore)表示资源的数目,主要用于实现进程间的互斥与同步,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。由于在Linux中,等待和信号具有特殊含义,所以对信号量的这两种操作常被称呼为P、V操作,P、V操作是两种原子操作。V操作会增加信号量SV的数值,P操作会减少它。P操作是用在进入临界区之前,V操作是用在退出临界区之后,这两个操作必须成对出现。假设有信号量SV,则对它的P、V操作含义如下:
- P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
- V(SV):如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。
信号量跟信号没有任何关系。信号量又分为两种:二进制信号量和计数信号量。最常用、最简单的信号量是二进制信号量,只能取0和1两个值,二进制信号量又称为互斥锁。P、V操作解释如下:
P(sema) //sema:信号量
{
while(sema == 0); //当信号量为0时,进行等待操作,此时进程被挂起,直到sema不为0
sema = sema - 1; //sema大于0,进程进入临界区,sema值减1
}
V(sema)
{
sema = sema + 1; //进程执行完任务,从临界区出来,sema的值增加1
}
13.5.2 信号量函数
功能:初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
参3:value指定信号量初值
功能:以原子操作的方式将信号量的值减1 --
int sem_wait(sem_t *sem);
功能:为sem_wait()的非阻塞版,不进行等待。如果信号量计数大于0,则信号量立即减1并返回0,否则立即返回-1
int sem_trywait(sem_t *sem);
功能:与 sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参2:abs_timeout采用的是绝对时间,由自1970-01-01 00:00:00 +0000(UTC) 秒数和纳秒数构成。
struct timespec {
time_t tv_sec;
long tv_nsec;
};
sem_timedwait存在缺陷:假设当前系统时间是1565000000(2019-08-05 18:13:20),
sem_timedwait传入的阻塞等待的时间戳是1565000100(2019-08-05 18:15:00),
那么sem_timedwait就需要阻塞1分40秒(100秒),若在sem_timedwait阻塞过程中,
中途将系统时间往前修改成1500000000(2017-07-14 10:40:00),
那么sem_timedwait此时就会阻塞2年多!
返回值:
如果信号量大于0,则对信号量进行递减操作并立马返回正常;
如果信号量小于0,则阻塞等待,当阻塞超时时返回失败(errno 设置为 ETIMEDOUT)。
功能:以原子操作的方式将信号量的值加1 ++
int sem_post(sem_t *sem);
功能:销毁一个信号量
int sem_destroy(sem_t *sem);
功能:读取sem中信号量计数,放到sval指向的整数上
int sem_getvalue(sem_t * sem, int * sval);
所有这些函数在成功时都返回 0;错误保持信号量值没有更改,-1 被返回,并设置 errno



