Skip to content

进程

进程基本

四要素

  1. 有一段程序供其执行。不一定是进程所专有,可以与其他进程共用

  2. 有起码的“私有财产”,这就是进程的专用的系统堆栈空间

  3. “户口”,这就是在内核中有一个task_struct的数据结构,或成为“进程控制块” ,有了这个能成为内核调度的一个基本单位接受内核的调度。

  4. 有独立的储存空间,意味着有专有的用户空间。进一步意味着出来前述的系统空间堆栈还有专用的用户空间堆栈

这四条是必要的。如果只具备前面两条而缺了第四条,那就成为“线程”, 如果没有用户空间就称为“内核线程”。如果共享用户空间就称为“用户空间”

进程终止

8种方式是进程终止,其中5种是正常方式:

  1. 从main 返回
  2. 调用 exit
  3. 调用_exit_Exit
  4. 最后一个线程从其启动例程返回
  5. 从最后一个线程调用pthread_exit

异常终止

  1. 调用abort
  2. 接收到一个信号
  3. 最后一个线程对取消请求作出响应

进程控制

函数fork

fork函数被调用一次返回两次,子进程返回值是0,父进程返回值是子进程的ID

vfork

vforkfork区别是:vfork保证子进程先运行,在它调用execexit之后父进程才可能被调度运行。当子进程调用这两个函数任意一个时,父进程会恢复运行。(如果调用这两个函数之前子进程依赖父进程的进一步动作,则会死锁)

exit

3个终止函数(exit,_exit,_Exit)将其退出状态作为参数传送给函数。在异常终止情况,内核产生一个指示异常终止原因的中止状态,父进程用waitwaitpid获取其状态

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:显示与作业相关的信息

编程规则

  1. 调用umask将文件模式创建屏蔽字设置为一个已知值

  2. 调用fork ,然后使父进程exit。

  3. 调用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);
    }


}

进程间通信

管道

无名管道

  1. 一般是半双工的
  2. 只能在具有公共祖先的两个进程之间使用
#include <unistd.h>
int pipe(int fd[2]);

fd但会两个文件描述符,fd[0]为读打开,fd[1]为写打开

当管道的一端被关闭后,下列两条规则起作用

  1. 当读一个写端已被关闭的管道时,所有数据读出后,read返回0,表示文件结束。
  2. 如果写一个读端已被关闭的管道时,则产生信号 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.gidsem_perm.mode。此命令只能由下列两种进程执行:一种是其有 效用户ID等于sem_perm.cuidsem_perm.uid的进程;另一种是具有超级用户特权的进程。
  • IPC_RMID从系统中删除该信号量集合。这种删除是立即的。仍在使用此信号量的 其他进程在它们下次意图对此信号量进行操作时,将出错返回EIDRM。此命令只能由下列两 种进程执行:一种是具有效用户ID等于sem_perm.cuidsem_perm.uid的进程;另一种是具有超 级用户特权的进程。
  • GETVAL返回成员semnumsemval值。
  • SETVAL设置成员semnumsemval值。该值由arg.val指定。
  • GETPID返回成员semnumsempid值。
  • GETNCNT返回成员semnumsemncnt值。
  • GETZCNT返回成员semnumsemzcnt值。
  • GETALL取该集合中所有信号量的值,并将它们存放在由arg.array指向的数组中。
  • SETALL按arg.array指向的数组中的值设置该集合中所有信号量的值。

semopsembuf结构体中的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种方式退出

  1. 线程可以简单的从启动例程中返回,返回值是线程的退出码
  2. 线程可以被同一进程中的其他线程取消
  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 */
避免死锁

例如,程序使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥量,并在试图锁住第二个时处于阻塞,但是拥有第二个互斥量的线程也试图锁住第一个,于是产生死锁

避免死锁发生

  1. 多个互斥量注意排序
  2. 使用 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种方式处理

  1. 忽略此信号。大多数可以这样处理,但是SIGKILL,SIGSTOP 不能被忽略
  2. 捕捉信号。要通知内核在某种信号发生时,调用一个用户函数
  3. 执行系统默认动作
#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)

killpid参数有以下4种情况

  • pid > 0: 将信号发送给ID为pid的进程
  • pid == 0: 将信号发送给与发送进程属于同一进程组的所有进程
  • pid < 0: 将信号发送给进程组ID等于pid绝对值
  • pid == -1: 将信号发送给发送进程有权限向他们发信号的所有进程
unsigned int alarm(unsigned int seconds);

/* 使用调用进程挂起直至捕捉到一个信号 */
int pause(void);