概念:
进程:一个程序在一个数据集合上的一次运行过程,是分配和管理资源的基本单位。
线程:线程是进程中的一个实体,是被系统调度和执行的基本单位。一个进程可以包含多个线程。
需要做区分的两个常用名词:
作业:作业是用户提交给系统的一个任务,一个作业通常包括几个进程,几个进程共同完成一个任务,即作业。
管程:管程实际上是定义了一个数据结构和在该数据结构上的能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。
使用:
基本进程控制原语:
用fork可以创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。
记住这句话,下面围绕这句话展开,介绍这些函数怎么实现的。
进程的创建:
Linux下用fork函数创建子进程。
fork函数:用来创建一个新的进程,成为子进程,子进程和父进程是两个完全不同的进程,对于父进程,fork函数返回子进程的进程号,对于子进程,fork函数返回0。(进程号是非负整数,如果fork创建过程中返回负数,则代表创建进程出错)
Demo:
#include "stdio.h"
//fork函数的基本使用框架:
int main(void)
{
int i;
if(fork() == 0)
{
//子进程程序
for(i = 0;i < 1000 ; i++)
printf("This is child process \n");
}
else
{
//父进程程序
for( i = 0 ; i<1000 ; i++)
printf("This is process process \n");
}
}
在Linux上执行该代码,就能看到屏幕上交替出现子进程和和父进程各打印出的一千条信息了。
子进程一但开始运行,虽然他继承了父进程的一切数据,但实际上数据已经分开了,相互之间不再有影响了,也就是说他们之间不再共享数据了。子进程是父进程的一个副本。
再看一个复杂 完善一点的:
#include "apue.h"
int globvar = 6; /* external variable in initialized data */
char buf[] = "a write to stdout\n";
int
main(void)
{
int var; /* automatic variable on the stack */
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
err_sys("write error");
printf("before fork\n"); /* we don't flush stdout */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
/* child */
globvar++; /* modify variables */
var++;
} else {
sleep(2); /* parent */
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
var);
exit(0);
}
来执行上面的程序,得到如下的结果
$ ./a.out
a write to stdout
before fork
pid = 430, glob = 7, var = 89 //子进程的变量值改变了
pid = 429, glob = 6, var = 88//父进程的变量值没有改变
$ a.out > temp.out
$ cat temp.out
a write to stdout
before fork
pid = 432, glob = 7, var = 89
before fork
pid = 431, glob = 6 ,var = 88
通过第一个执行结果可以发现,父进程和子进程里面的内容各执行了一次,main函数里的printf执行了两次,这便是多进程。
再看第二个fork与IO函数的交互关系,并且文件重定向输出结果,与第一个不一样。代码中write函数是不带缓冲的(文件IO),而标准IO里的fwrite是带有缓冲区的。如果标准输出连接到终端设备,则它是行缓冲的,否则是全缓冲。当以交互方式运行该程序时,只得到该printf输出的行一次,原因是标准输出缓冲区由换行符冲洗。但是将标准输出重定向到一个文件时,却得到printf输出行两次。原因是在fork之前调用了printf一次,但当调用fork时,该行数据扔在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有的缓冲区中。当每个进程终止时,将缓冲区中的内容都被写到响应文件中。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,如果需要确定,需要使用进程间通信。
vfork函数:与fork类似。vfork保证子进程先进行,在它调用exec或exit之后父进程在可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
进程的终止:
正常终止:
1、exit函数:调用各终止处理程序,然后关闭所有标准IO流。等效于在main函数最后调用return。
2、_exit或_Exit函数。为进程提供一种无需运行终止处理程序或信号处理程序而终止的办法。
3、进程的最后一个线程调用pthread_exit函数。这时进程终止状态总是0,这与传送给pthread_exit的参数无关。
4、在main函数内执行return语句。
5、进程的最后一个线程在其启动例程中执行return语句。
异常终止:
1、调用abort。
2、当进程接收到某些信号时。
3、最后一个线程对“取消”请求做出响应。
还有一个重要的函数:
**wait函数:**当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。
调用wait会发生如下事情:
1、如果其所有子进程都还在运行,则阻塞。
2、如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3、如果它没有任何子进程,则立即出错返回。
对比:
exit:结束当前进程,关闭该进程所有标准IO流。相当于在main最后用return。
wait:多用于父进程对子进程的回收。
exec族函数:
用fork函数创建新的子进程后,子进程往往要调用一种extc函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。
再来回顾一下进程控制原语:
用fork可以创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。
有7种不同的exec函数
execl execv execle execve execlp execvp fexecve。
下面演示一个exec函数例子
#include "apue.h"
#include <sys/wait.h>
char *env_init[] = {
"USER=unknown", "PATH=/tmp", NULL };
int
main(void)
{
pid_t pid;
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
/* specify pathname, specify environment */
if (execle("/home/sar/bin/echoall", "echoall", "myarg1",
"MY ARG2", (char *)0, env_init) < 0)
err_sys("execle error");
}
if (waitpid(pid, NULL, 0) < 0)
err_sys("wait error");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
/* specify filename, inherit environment */
if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
err_sys("execlp error");
}
exit(0);
}
该程序中先调用execle,它要求一个路径名和一个特定的环境。下一个调用的是execlp,它用一个文件名,并将调用者的环境传送给新程序。
以上就是进程常用的控制语句。
下面简单介绍一下线程。
每个进程有进程ID,每个线程也有线程ID。进程ID在整个系统中是唯一的,但线程ID只有在它所属的进程上下文中才有意义。相当于变量中的局部变量。
线程创建:
新增的线程可以通过调用pthread_create函数创建,原型如下:
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *),void *restrict arg);
当pthread_create成功返回时,新创建线程的线程ID会被设置成tidp指向的内存单元。attr参数用于定制各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数的传入。
创建线程的Demo:
#include "apue.h"
#include <pthread.h>
pthread_t ntid;
void
printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,
(unsigned long)tid, (unsigned long)tid);
}
void *
thr_fn(void *arg)
{
printids("new thread: ");
return((void *)0);
}
int
main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread:");
sleep(1);
exit(0);
}
上面的例子有两个特别之处:
1、主线程需要休眠,如果主线程不休眠,它可能会退出,这样新线程还没有机会运行,真个进程可能就已经终止了。
2、新线程是通过调动pthread_self函数获取自己的线程ID的,而不是从共享内存中读出的,或者从线程的启动例程中以参数的形式接收到的。
运行结果如下:
$./a.out
main thread: pid 20075 tid 1 (0x1)
new thread: pid 20075 tid 2 (ox2)
线程的终止:
调用pthread_exit来终止线程。
当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。
线程同步:
如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性的问题。但是,当一个线程可以修改的变量,其它线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保他们在访问变量的存储内容时不会访问到无效的值。
为了解决这个问题,线程可以使用锁,保证同一时间只允许一个线程访问该变量。或者使用互斥量。其实互斥量本质就是一把锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞,直到当前线程释放该互斥锁。```
结束,来自《UNIX环境高级编程》。
文章评论