进程
进程基本
四要素
-
有一段程序供其执行。不一定是进程所专有,可以与其他进程共用
-
有起码的“私有财产”,这就是进程的专用的系统堆栈空间
-
有“户口”,这就是在内核中有一个
task_struct的数据结构,或成为“进程控制块” ,有了这个能成为内核调度的一个基本单位接受内核的调度。 -
有独立的储存空间,意味着有专有的用户空间。进一步意味着出来前述的系统空间堆栈还有专用的用户空间堆栈
这四条是必要的。如果只具备前面两条而缺了第四条,那就成为“线程”, 如果没有用户空间就称为“内核线程”。如果共享用户空间就称为“用户空间”
进程终止
8种方式是进程终止,其中5种是正常方式:
- 从main 返回
- 调用
exit - 调用
_exit或_Exit - 最后一个线程从其启动例程返回
- 从最后一个线程调用
pthread_exit
异常终止
- 调用
abort - 接收到一个信号
- 最后一个线程对取消请求作出响应
进程控制
函数fork
fork函数被调用一次返回两次,子进程返回值是0,父进程返回值是子进程的ID
vfork
vfork 和fork区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当子进程调用这两个函数任意一个时,父进程会恢复运行。(如果调用这两个函数之前子进程依赖父进程的进一步动作,则会死锁)
exit
3个终止函数(exit,_exit,_Exit)将其退出状态作为参数传送给函数。在异常终止情况,内核产生一个指示异常终止原因的中止状态,父进程用wait和waitpid获取其状态
wait和waitpid
进程终止时,内核就像其父进程发送SIGCHLD信号,这是个异步事件,父进程可以选择忽略该信号,或者提供处理函数。
#include <sys/wait.h>
pid_t wait(int *static);
pid_t wait_pid(pit_t pid, int *static, int options)
如果子进程已经终止,wait立即返回并取得子进程的状态,否则阻塞,直到子进程结束
wait_pid中的pid参数作用如下;
pid == -1: 等待任一子进程pid > 0: 等待进程ID与pid相等的子进程pid == 0: 等待组ID等于调用进程组ID的任一子进程pid < -1: 等待组ID等于pid绝对值的任一子进程
options参数:
| 常量 | 说明 |
|---|---|
| WCONTINUED | 若实现支持作业控制,由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态 |
| WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时返回值是0 |
| WUNTRACED | 若实现支持作业控制,由pid指定的任一子进程已经停止状态,并且其状态尚未报告,则返回其状态 |
exec
#include <unistd.h>
int execl(const char *pathname, const char * arg0, ... /* (char *) 0 */);
int execv(const char *pathname, char *const argv[] );
int execle(const char *pathname, const char * arg0, ...
/* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *constenvp[] );
int execlp(const char *filename, const char * arg0, ... /* (char *) 0 */);
int execvp(const char *filename, char *const argv[] );
六个函数返回:若出错则为- 1,若成功则不返回
system
进程关系
孤儿进程
父进程先退出
僵尸进程
守护进程
ps命令
打印系统各个进程的状态
ps -axj
-a: 显示有其他用户所拥有的进程的状态-x:显示没有控制终端的进程状态-j:显示与作业相关的信息
编程规则
-
调用
umask将文件模式创建屏蔽字设置为一个已知值 -
调用fork ,然后使父进程exit。
-
调用
setsid创建新会话,
使调用进程:
- 成为新会话的首进程
- 成为一个新进程组的组长进程
-
没有控制终端
-
将当前目录更改为根目录
-
关闭不在需要的文件描述符
-
某些守护进程打开
/dev/null使其具有文件描述符0,1和2,这样任何一个视图读stdin,stdout和stderr的库例程都不会产生任何效果
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
void daemoize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/* clear file creation mask */
umask(0);
/*get maxinum number of file descriptors*/
if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
err_quit("%s: can`t get file limit",cmd);
}
/*become a session leader to lose controlling TTY*/
if ((pid = fork()) < 0) {
err_quit("%s: can`t fork",cmd);
} else if(pid != 0) {
exit(0);
}
setsid();
/* change the current working directory to the root so
* we won`t prevent file systems form being unmounted
*/
if(chdir("/") < 0) {
err_quit("%s,can`t change directory to /",cmd);
}
/* close all open file descriptors*/
if (rl.rlim_max == RLIM_INFINITY) {
rl.rlim_max = 1024;
}
for (i = 0; i < rl.rlim_max; i++) {
close(i);
}
/* attach file descriptors 0,1,2 to /dev/null */
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
/* initialize the log file*/
openlog(cmd,LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1|| fd2 != 2) {
syslog(LOG_ERR,"unexpected file decriptors");
exit(1);
}
}
进程间通信
管道
无名管道
- 一般是半双工的
- 只能在具有公共祖先的两个进程之间使用
#include <unistd.h>
int pipe(int fd[2]);
fd但会两个文件描述符,fd[0]为读打开,fd[1]为写打开
当管道的一端被关闭后,下列两条规则起作用
- 当读一个写端已被关闭的管道时,所有数据读出后,read返回0,表示文件结束。
- 如果写一个读端已被关闭的管道时,则产生信号
SIGPIPE
FIFO(有名管道)
不相关的进程也能交换数据
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
消息队列
#include <sys/msg.h>
int msgget(key_t key, int flag);
/* cmd指定执行的命令
* IPC_STAT: 取次队列的msqid_ds结构,放入buf中
* IPC_SET: 设置队列的msqid_ds结构体
* IPC_RMID: 删除该消息队列
*/
int msgctl(int msgpid, int cmd, struct msqid_ds *buf);
int msgsnd(int msgid, const void * ptr, size_t nbytes, int flags);
int msgrcv(int msgid, const void * ptr, size_t nbytes, long type, int flags);
消息队列发送中的ptr 指向结构体,
struct mymsg {
long type;
char mtext[32];
}
nbytes长度为 sizof(struct mymsg) - sizeof (long); 若flag 定义为IPC_NOWAIT则立即返回,不会阻塞。
消息队列接收中的type使用如下;
type == 0: 返回队列的第一个消息type > 0: 返回队列中消息类型为type的第一个消息type < 0: 返回队列中消息类型值小于type绝对值得消息,若有若干个,则取类型值最小的
信号量
#include <sys/sem.h>
int semget(key_t key, int mems, int flags);
union semun {
int value; /* for SETVAL*/
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL*/
};
int semctl(int semid, int semnum, int cmd,... /* union semun arg*/);
struct sembuf{
ushort sem_num;/*member#inset(0,1,…,nsems-1*/
short sem_op; /*operation(negative,0,orpasitive*/)
short sem_flg;/*IPC_NOWAIT,SEM_UNDO*/
};
int semop(int semid, struct sembuf semoparray[], size_t nops);
cmd 参数指定了10中命令
- IPC_STAT对此集合取
semid_ds结构,并存放在由arg.buf指向的结构中。 - IPC_SET按由
arg.buf指向的结构中的值设置与此集合相关结构中的下列三个字段值:sem_perm.uid,sem_perm.gid和sem_perm.mode。此命令只能由下列两种进程执行:一种是其有 效用户ID等于sem_perm.cuid或sem_perm.uid的进程;另一种是具有超级用户特权的进程。 - IPC_RMID从系统中删除该信号量集合。这种删除是立即的。仍在使用此信号量的
其他进程在它们下次意图对此信号量进行操作时,将出错返回EIDRM。此命令只能由下列两
种进程执行:一种是具有效用户ID等于
sem_perm.cuid或sem_perm.uid的进程;另一种是具有超 级用户特权的进程。 - GETVAL返回成员
semnum的semval值。 - SETVAL设置成员
semnum的semval值。该值由arg.val指定。 - GETPID返回成员
semnum的sempid值。 - GETNCNT返回成员
semnum的semncnt值。 - GETZCNT返回成员
semnum的semzcnt值。 - GETALL取该集合中所有信号量的值,并将它们存放在由
arg.array指向的数组中。 - SETALL按
arg.array指向的数组中的值设置该集合中所有信号量的值。
semop中sembuf结构体中的sme_op为正,返回进程占用的资源,为负获取该资源。
共享内存
共享存储允许两个或多个进程共享一定的存储区,信号量被用来实现共享存储的同步。
#incldue <sys/shm.h>
int shmget(key_t key, int size, int flag);
/* cmd : IPC_STAT,IPC_SET,IPC_RAMID,SHM_LOCK, SHM_UNLOCK*/
int shmctl(int shmid, int cmd, struct shmid_ds buf);
/*addr 为0 此段连接到有内核选择的第一个地址,推荐使用*/
void *shmat(int shmid, void addr, int flag);
int shmdt(void addr);
线程
就像进程ID一样,每个线程也有一个ID,进程ID在整个系统是唯一的,但线程ID只有在它所属的进程上下文中有意义。
线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
/* 成功返回 0 */
线程的终止
如果进程中的任意线程调用了exit,_exit,_Exit那么整个进程就会终止。
单个进程可以通过3种方式退出
- 线程可以简单的从启动例程中返回,返回值是线程的退出码
- 线程可以被同一进程中的其他线程取消
- 线程调用
pthread_exit
void pthread_exit(void *rval_ptr);
/* rval_ptr 是一个无类型的指针,与传给启动例程的单个参数类似,进程中的其他线程可以调用 pthread_join访问到这个指针 */
int pthread_join(pthread_t thread, void ** rval_ptr)
pthread_join一直阻塞,直到指定线程终止
int pthread_cancel(pthread_t tid);
/* 仅仅提出请求,线程可选择忽略 */
线程同步
互斥量
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 不阻塞 ,有锁时就失败返回EBUSY*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 成功返回 0 */
避免死锁
例如,程序使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥量,并在试图锁住第二个时处于阻塞,但是拥有第二个互斥量的线程也试图锁住第一个,于是产生死锁
避免死锁发生
- 多个互斥量注意排序
- 使用
pthread_mutex_trylock
带有超时的读写锁
条件变量
条件变量给多个线程提供了一个会合的场所,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生
int pthread_cond_init(pthread_cond_t *restrict_cond,
const pthread_condattr_t *restrict_attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict_cond, pthread_mutex_t *restrict_cond);
int pthread_cond_timewait(pthread_cond_t *restrict_cond,
pthread_mutex_t *restrict_cond,
const struct timespec * restrict tsptr);
自旋锁
它与互斥量类似,但是不通过休眠使进程阻塞,而是获取锁之前一直处于忙等待阻塞状态,自旋锁用于:锁被持有的时间短,且线程不希望在重新调度上花费太多成本。
信号
信号是软件中断,首先每个信号都有一个名字,SIG 开头。当某个信号出现时候,可以告诉内核按下列3种方式处理
- 忽略此信号。大多数可以这样处理,但是
SIGKILL,SIGSTOP不能被忽略 - 捕捉信号。要通知内核在某种信号发生时,调用一个用户函数
- 执行系统默认动作
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/* 将信号发送给进程或进程组 */
int kill(pid_t pid, int signo);
/* 允许进程向自身发送信号 */
int raise(int signo)
kill的pid参数有以下4种情况
pid > 0: 将信号发送给ID为pid的进程pid == 0: 将信号发送给与发送进程属于同一进程组的所有进程pid < 0: 将信号发送给进程组ID等于pid绝对值pid == -1: 将信号发送给发送进程有权限向他们发信号的所有进程
unsigned int alarm(unsigned int seconds);
/* 使用调用进程挂起直至捕捉到一个信号 */
int pause(void);